Skip to content

Commit b722d3f

Browse files
authored
perf(InViewport): use intersection observer to calculate inviewport state (#25)
Replace custom logic to calculate `InViewport` status for native `IntersectionObserver` API to improve performance and clean up code BREAKING CHANGE: Removed `forRoot` method in module which is no longer required for `AppBrowserModule`. Replaced with `forServer` method for `AppServerModule`. Removed debounce feature and rxjs dependancy to leave implementation up to the consumer of the library. This reduces bundle size if debounce feature is not being used. Updated inviewport classes to `sn-inviewport--in` and `sn-inviewport--out` to match SOON styleguide
1 parent c3a01c6 commit b722d3f

33 files changed

+7123
-6102
lines changed

.travis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ sudo: false
33

44
language: node_js
55
node_js:
6-
- "8"
6+
- '8'
77

88
addons:
99
apt:
@@ -14,7 +14,7 @@ addons:
1414

1515
cache:
1616
directories:
17-
- ./node_modules
17+
- ./node_modules
1818

1919
install:
2020
- npm i --no-progress

README.md

Lines changed: 71 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,21 @@
44
[![Coverage Status][coveralls-badge]][coveralls-badge-url]
55
[![Commitizen friendly][commitizen-badge]][commitizen]
66

7-
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.5.4.
7+
A simple lightweight library for [Angular][angular] that detects when an element is within the browser viewport and adds a `sn-viewport--in` or `sn-viewport--out` class to the element.
88

9-
A simple lightweight library for [Angular][angular] that detects when an element is within the browser viewport and adds a `sn-viewport-in` or `sn-viewport-out` class to the element.
10-
11-
This is a simple library for [Angular][angular], implemented in the [Angular Package Format v5.0](https://docs.google.com/document/d/1CZC2rcpxffTDfRDs6p1cfbmKNLA6x5O-NtkJglDaBVs/edit#heading=h.k0mh3o8u5hx).
9+
This is a simple library for [Angular][angular], implemented in the [Angular Package Format v5.0][apfv5].
1210

1311
## Install
1412

1513
### via NPM
1614

17-
```
18-
npm i @thisissoon/angular-inviewport --save
15+
```bash
16+
npm i @thisissoon/angular-inviewport
1917
```
2018

2119
### via Yarn
2220

23-
```
21+
```bash
2422
yarn add @thisissoon/angular-inviewport
2523
```
2624

@@ -29,34 +27,52 @@ yarn add @thisissoon/angular-inviewport
2927
```ts
3028
import { InViewportModule } from '@thisissoon/angular-inviewport';
3129

32-
const providers = [{ provide: WindowRef, useFactory: () => window }];
33-
3430
@NgModule({
35-
imports: [
36-
// provide WindowRef class by using an window object
37-
InViewportModule.forRoot(providers)
38-
]
31+
imports: [InViewportModule]
3932
})
4033
export class AppModule {}
4134
```
4235

43-
`app.server.module.ts` // Only required if using Angular Universal
36+
`app.server.module.ts`
37+
Only required For Server Side Rendering
4438

4539
```ts
4640
import { InViewportModule } from '@thisissoon/angular-inviewport';
4741

4842
@NgModule({
49-
imports: [
50-
// no need to pass any arguments to forRoot
51-
// function for server module
52-
InViewportModule.forRoot()
53-
]
43+
imports: [InViewportModule.forServer()]
5444
})
5545
export class AppServerModule {}
5646
```
5747

48+
## Browser Support
49+
50+
This library makes use of the [Intersection Observer API][intersection-observer-api] which requires a [polyfill][intersection-observer-polyfill] to work on some browsers.
51+
52+
### Install the polyfill
53+
54+
```bash
55+
npm i intersection-observer
56+
```
57+
58+
Or use yarn
59+
60+
```bash
61+
yarn add intersection-observer
62+
```
63+
64+
### Include the polyfill
65+
66+
Add this somewhere in your `src/polyfills.ts` file
67+
68+
```ts
69+
import 'intersection-observer';
70+
```
71+
5872
## Examples
5973

74+
A working example can be found [here](https://github.com/thisissoon/angular-inviewport/tree/master/src) folder.
75+
6076
### Just using classes
6177

6278
#### `app.component.html`
@@ -72,11 +88,11 @@ export class AppServerModule {}
7288
transition: transform 0.35s ease-out;
7389
}
7490

75-
.foo.sn-viewport-out {
91+
.foo.sn-viewport--out {
7692
transform: translateY(-30px);
7793
}
7894

79-
.foo.sn-viewport-in {
95+
.foo.sn-viewport--in {
8096
transform: translateY(0);
8197
}
8298
```
@@ -86,7 +102,10 @@ export class AppServerModule {}
86102
#### `app.component.html`
87103

88104
```html
89-
<p class="foo" snInViewport (inViewportChange)="onInViewportChange($event)">
105+
<p
106+
class="foo"
107+
snInViewport
108+
(inViewportChange)="onInViewportChange($event)">
90109
Amet tempor excepteur occaecat nulla.
91110
</p>
92111
```
@@ -111,42 +130,40 @@ export class AppComponent {
111130
}
112131
```
113132

114-
### Specify debounce time (default: 100ms)
133+
### Debounce example
115134

116135
#### `app.component.html`
117136

118137
```html
119-
<p class="foo" snInViewport [debounce]="500">
138+
<p
139+
class="foo"
140+
snInViewport
141+
(inViewportChange)="onInViewportChange($event)">
120142
Amet tempor excepteur occaecat nulla.
121143
</p>
122144
```
123145

124-
### Specify parent scrollable element
125-
126-
Useful if element is within another scrollable element
127-
128-
#### `app.component.html`
129-
130-
```html
131-
<div #container>
132-
<p class="foo" snInViewport [debounce]="500" [parent]="container">
133-
Amet tempor excepteur occaecat nulla.
134-
</p>
135-
</div>
136-
```
146+
#### `app.component.ts`
137147

138-
### Trigger inviewport check manually
148+
```ts
149+
import { Subject } from 'rxjs';
150+
import { debounceTime } from 'rxjs/operators';
139151

140-
Window scroll and resize events doesn't cover all potential use cases for the inViewport status check. For example if using directive inside a carousel. To trigger a check manually simply assign a template variable value to the directive and call `calculateInViewportStatus` when you require.
152+
export class AppComponent {
153+
inViewportChange: Subject<boolean>;
141154

142-
#### `app.component.html`
155+
constructor() {
156+
this.inViewportChange = new Subject<boolean>().pipe(debounceTime(300));
143157

144-
```html
145-
<p snInViewport #foo="snInViewport">
146-
Amet tempor excepteur occaecat nulla.
147-
</p>
158+
this.inViewportChange.subscribe((inViewport: boolean) =>
159+
console.log(`element is in viewport: ${inViewport}`)
160+
);
161+
}
148162

149-
<button (click)="foo.calculateInViewportStatus()">Check status</button>
163+
onInViewportChange(inViewport: boolean) {
164+
this.inViewportChange.next(inViewport);
165+
}
166+
}
150167
```
151168

152169
## Development server
@@ -161,6 +178,12 @@ Run `ng generate component component-name` to generate a new component. You can
161178

162179
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build.
163180

181+
## Server side rendering
182+
183+
The app can be rendered on a server before serving pages to the client. Server side rendering is achieved using [Express](https://expressjs.com/) and [Angular Universal](https://github.com/angular/universal) with [Express](https://expressjs.com/) running a [node](https://nodejs.org/en/) web server and [@nguniversal/express-engine](https://github.com/angular/universal/tree/master/modules/express-engine) providing a template engine for [Express](https://expressjs.com/) to render the angular pages.
184+
185+
Run `npm run build:ssr && npm run serve:ssr` to build client and server bundles and run an express app which will render the angular templates before being sent to the client. Navigate to `http://localhost:4000/` to view the SSR version of the app.
186+
164187
## Running unit tests
165188

166189
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
@@ -190,3 +213,6 @@ To get more help on the Angular CLI use `ng help` or go check out the [Angular C
190213
[commitizen-badge]: https://img.shields.io/badge/commitizen-friendly-brightgreen.svg
191214
[conventional-changelog]: https://github.com/conventional-changelog/conventional-changelog
192215
[standard-version]: https://github.com/conventional-changelog/standard-version
216+
[apfv5]: https://docs.google.com/document/d/1CZC2rcpxffTDfRDs6p1cfbmKNLA6x5O-NtkJglDaBVs/edit#heading=h.k0mh3o8u5hx
217+
[intersection-observer-api]: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
218+
[intersection-observer-polyfill]: https://github.com/w3c/IntersectionObserver/tree/master/polyfill

angular.json

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"build": {
1212
"builder": "@angular-devkit/build-angular:browser",
1313
"options": {
14-
"outputPath": "dist",
14+
"outputPath": "dist/angular-inviewport",
1515
"index": "src/index.html",
1616
"main": "src/main.ts",
1717
"tsConfig": "src/tsconfig.app.json",
@@ -68,7 +68,8 @@
6868
"scripts": [],
6969
"progress": false,
7070
"styles": ["src/styles.scss"],
71-
"assets": ["src/assets", "src/favicon.ico"]
71+
"assets": ["src/assets", "src/favicon.ico"],
72+
"codeCoverageExclude": ["src/app/window/**"]
7273
}
7374
},
7475
"lint": {
@@ -77,6 +78,14 @@
7778
"tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json"],
7879
"exclude": ["**/node_modules/**"]
7980
}
81+
},
82+
"server": {
83+
"builder": "@angular-devkit/build-angular:server",
84+
"options": {
85+
"outputPath": "dist/angular-inviewport-server",
86+
"main": "src/main.server.ts",
87+
"tsConfig": "src/tsconfig.server.json"
88+
}
8089
}
8190
}
8291
},
@@ -111,5 +120,8 @@
111120
"@schematics/angular:directive": {
112121
"prefix": "sn"
113122
}
123+
},
124+
"cli": {
125+
"packageManager": "npm"
114126
}
115127
}

e2e/app.e2e-spec.ts

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { browser, element, by } from 'protractor';
22
import { AppPage } from './app.po';
33

4-
describe('InViewport Lib E2E Tests', function () {
5-
4+
describe('InViewport Lib E2E Tests', function() {
65
const page = new AppPage();
76

87
beforeEach(() => page.navigateTo());
@@ -12,58 +11,89 @@ describe('InViewport Lib E2E Tests', function () {
1211
beforeEach(() => page.scrollTo());
1312

1413
afterEach(() => {
15-
browser.manage().logs().get('browser').then((browserLog: any[]) => {
16-
expect(browserLog).toEqual([]);
17-
});
14+
browser
15+
.manage()
16+
.logs()
17+
.get('browser')
18+
.then((browserLog: any[]) => {
19+
expect(browserLog).toEqual([]);
20+
});
1821
});
1922

2023
it('should display lib', () => {
21-
expect(element(by.css('p')).getText()).toContain('Amet tempor excepteur occaecat nulla.');
24+
expect(element(by.css('p')).getText()).toContain(
25+
'Amet tempor excepteur occaecat nulla.'
26+
);
2227
});
2328

24-
it('should show `sn-viewport-out` class', () => {
25-
expect(page.getSmallElement().getAttribute('class')).toContain('sn-viewport-out');
29+
it('should show `sn-viewport--out` class', () => {
30+
expect(page.getSmallElement().getAttribute('class')).toContain(
31+
'sn-viewport--out'
32+
);
2633

2734
page.scrollTo(0, 768 / 2);
28-
expect(page.getSmallElement().getAttribute('class')).not.toContain('sn-viewport-out');
35+
expect(page.getSmallElement().getAttribute('class')).not.toContain(
36+
'sn-viewport--out'
37+
);
2938
});
3039

31-
it('should show `sn-viewport-in` class', () => {
40+
it('should show `sn-viewport--in` class', () => {
3241
page.scrollTo(0, 768 / 2);
33-
expect(page.getSmallElement().getAttribute('class')).toContain('sn-viewport-in');
42+
expect(page.getSmallElement().getAttribute('class')).toContain(
43+
'sn-viewport--in'
44+
);
3445

3546
page.scrollTo(0, 0);
36-
expect(page.getSmallElement().getAttribute('class')).not.toContain('sn-viewport-in');
47+
expect(page.getSmallElement().getAttribute('class')).not.toContain(
48+
'sn-viewport--in'
49+
);
3750
});
3851

3952
it('should run event handler `onInViewportChange`', () => {
4053
page.scrollTo(0, 768 / 2);
4154
expect(page.getSmallElement().getAttribute('class')).toContain('highlight');
4255

4356
page.scrollTo();
44-
expect(page.getSmallElement().getAttribute('class')).not.toContain('highlight');
57+
expect(page.getSmallElement().getAttribute('class')).not.toContain(
58+
'highlight'
59+
);
4560
});
4661

4762
it('should add `in-viewport` class to large element', () => {
4863
page.scrollTo(0, 768 * 2);
49-
expect(page.getLargeElement().getAttribute('class')).toContain('sn-viewport-in');
64+
expect(page.getLargeElement().getAttribute('class')).toContain(
65+
'sn-viewport--in'
66+
);
5067

5168
page.scrollTo();
52-
expect(page.getLargeElement().getAttribute('class')).not.toContain('sn-viewport-in');
69+
expect(page.getLargeElement().getAttribute('class')).not.toContain(
70+
'sn-viewport--in'
71+
);
5372
});
5473

55-
it('should add `in-viewport` class to element inside a scrollable element', () => {
74+
it('should add `sn-viewport` class to element inside a scrollable element', () => {
5675
page.scrollTo(0, 768 * 3);
57-
expect(page.getInsideScrollableElement().getAttribute('class')).not.toContain('sn-viewport-in');
58-
expect(page.getInsideScrollableElement().getAttribute('class')).toContain('sn-viewport-out');
76+
expect(
77+
page.getScrollableInnerElement().getAttribute('class')
78+
).not.toContain('sn-viewport--in');
79+
expect(page.getScrollableInnerElement().getAttribute('class')).toContain(
80+
'sn-viewport--out'
81+
);
5982

6083
page.scrollableElementScrollTop(768);
61-
expect(page.getInsideScrollableElement().getAttribute('class')).toContain('sn-viewport-in');
62-
expect(page.getInsideScrollableElement().getAttribute('class')).not.toContain('sn-viewport-out');
84+
expect(page.getScrollableInnerElement().getAttribute('class')).toContain(
85+
'sn-viewport--in'
86+
);
87+
expect(
88+
page.getScrollableInnerElement().getAttribute('class')
89+
).not.toContain('sn-viewport--out');
6390

6491
page.scrollableElementScrollTop(768 * 2);
65-
expect(page.getInsideScrollableElement().getAttribute('class')).not.toContain('sn-viewport-in');
66-
expect(page.getInsideScrollableElement().getAttribute('class')).toContain('sn-viewport-out');
92+
expect(
93+
page.getScrollableInnerElement().getAttribute('class')
94+
).not.toContain('sn-viewport--in');
95+
expect(page.getScrollableInnerElement().getAttribute('class')).toContain(
96+
'sn-viewport--out'
97+
);
6798
});
68-
6999
});

0 commit comments

Comments
 (0)