Skip to content

Commit

Permalink
perf(InViewport): use intersection observer to calculate inviewport s…
Browse files Browse the repository at this point in the history
…tate (#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
  • Loading branch information
edoparearyee committed Oct 8, 2018
1 parent c3a01c6 commit b722d3f
Show file tree
Hide file tree
Showing 33 changed files with 7,123 additions and 6,102 deletions.
4 changes: 2 additions & 2 deletions .travis.yml
Expand Up @@ -3,7 +3,7 @@ sudo: false

language: node_js
node_js:
- "8"
- '8'

addons:
apt:
Expand All @@ -14,7 +14,7 @@ addons:

cache:
directories:
- ./node_modules
- ./node_modules

install:
- npm i --no-progress
Expand Down
116 changes: 71 additions & 45 deletions README.md
Expand Up @@ -4,23 +4,21 @@
[![Coverage Status][coveralls-badge]][coveralls-badge-url]
[![Commitizen friendly][commitizen-badge]][commitizen]

This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.5.4.
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.

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.

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).
This is a simple library for [Angular][angular], implemented in the [Angular Package Format v5.0][apfv5].

## Install

### via NPM

```
npm i @thisissoon/angular-inviewport --save
```bash
npm i @thisissoon/angular-inviewport
```

### via Yarn

```
```bash
yarn add @thisissoon/angular-inviewport
```

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

const providers = [{ provide: WindowRef, useFactory: () => window }];

@NgModule({
imports: [
// provide WindowRef class by using an window object
InViewportModule.forRoot(providers)
]
imports: [InViewportModule]
})
export class AppModule {}
```

`app.server.module.ts` // Only required if using Angular Universal
`app.server.module.ts`
Only required For Server Side Rendering

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

@NgModule({
imports: [
// no need to pass any arguments to forRoot
// function for server module
InViewportModule.forRoot()
]
imports: [InViewportModule.forServer()]
})
export class AppServerModule {}
```

## Browser Support

This library makes use of the [Intersection Observer API][intersection-observer-api] which requires a [polyfill][intersection-observer-polyfill] to work on some browsers.

### Install the polyfill

```bash
npm i intersection-observer
```

Or use yarn

```bash
yarn add intersection-observer
```

### Include the polyfill

Add this somewhere in your `src/polyfills.ts` file

```ts
import 'intersection-observer';
```

## Examples

A working example can be found [here](https://github.com/thisissoon/angular-inviewport/tree/master/src) folder.

### Just using classes

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

.foo.sn-viewport-out {
.foo.sn-viewport--out {
transform: translateY(-30px);
}

.foo.sn-viewport-in {
.foo.sn-viewport--in {
transform: translateY(0);
}
```
Expand All @@ -86,7 +102,10 @@ export class AppServerModule {}
#### `app.component.html`

```html
<p class="foo" snInViewport (inViewportChange)="onInViewportChange($event)">
<p
class="foo"
snInViewport
(inViewportChange)="onInViewportChange($event)">
Amet tempor excepteur occaecat nulla.
</p>
```
Expand All @@ -111,42 +130,40 @@ export class AppComponent {
}
```

### Specify debounce time (default: 100ms)
### Debounce example

#### `app.component.html`

```html
<p class="foo" snInViewport [debounce]="500">
<p
class="foo"
snInViewport
(inViewportChange)="onInViewportChange($event)">
Amet tempor excepteur occaecat nulla.
</p>
```

### Specify parent scrollable element

Useful if element is within another scrollable element

#### `app.component.html`

```html
<div #container>
<p class="foo" snInViewport [debounce]="500" [parent]="container">
Amet tempor excepteur occaecat nulla.
</p>
</div>
```
#### `app.component.ts`

### Trigger inviewport check manually
```ts
import { Subject } from 'rxjs';
import { debounceTime } from 'rxjs/operators';

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.
export class AppComponent {
inViewportChange: Subject<boolean>;

#### `app.component.html`
constructor() {
this.inViewportChange = new Subject<boolean>().pipe(debounceTime(300));

```html
<p snInViewport #foo="snInViewport">
Amet tempor excepteur occaecat nulla.
</p>
this.inViewportChange.subscribe((inViewport: boolean) =>
console.log(`element is in viewport: ${inViewport}`)
);
}

<button (click)="foo.calculateInViewportStatus()">Check status</button>
onInViewportChange(inViewport: boolean) {
this.inViewportChange.next(inViewport);
}
}
```

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

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.

## Server side rendering

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.

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.

## Running unit tests

Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
Expand Down Expand Up @@ -190,3 +213,6 @@ To get more help on the Angular CLI use `ng help` or go check out the [Angular C
[commitizen-badge]: https://img.shields.io/badge/commitizen-friendly-brightgreen.svg
[conventional-changelog]: https://github.com/conventional-changelog/conventional-changelog
[standard-version]: https://github.com/conventional-changelog/standard-version
[apfv5]: https://docs.google.com/document/d/1CZC2rcpxffTDfRDs6p1cfbmKNLA6x5O-NtkJglDaBVs/edit#heading=h.k0mh3o8u5hx
[intersection-observer-api]: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
[intersection-observer-polyfill]: https://github.com/w3c/IntersectionObserver/tree/master/polyfill
16 changes: 14 additions & 2 deletions angular.json
Expand Up @@ -11,7 +11,7 @@
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist",
"outputPath": "dist/angular-inviewport",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "src/tsconfig.app.json",
Expand Down Expand Up @@ -68,7 +68,8 @@
"scripts": [],
"progress": false,
"styles": ["src/styles.scss"],
"assets": ["src/assets", "src/favicon.ico"]
"assets": ["src/assets", "src/favicon.ico"],
"codeCoverageExclude": ["src/app/window/**"]
}
},
"lint": {
Expand All @@ -77,6 +78,14 @@
"tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json"],
"exclude": ["**/node_modules/**"]
}
},
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist/angular-inviewport-server",
"main": "src/main.server.ts",
"tsConfig": "src/tsconfig.server.json"
}
}
}
},
Expand Down Expand Up @@ -111,5 +120,8 @@
"@schematics/angular:directive": {
"prefix": "sn"
}
},
"cli": {
"packageManager": "npm"
}
}
76 changes: 53 additions & 23 deletions e2e/app.e2e-spec.ts
@@ -1,8 +1,7 @@
import { browser, element, by } from 'protractor';
import { AppPage } from './app.po';

describe('InViewport Lib E2E Tests', function () {

describe('InViewport Lib E2E Tests', function() {
const page = new AppPage();

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

afterEach(() => {
browser.manage().logs().get('browser').then((browserLog: any[]) => {
expect(browserLog).toEqual([]);
});
browser
.manage()
.logs()
.get('browser')
.then((browserLog: any[]) => {
expect(browserLog).toEqual([]);
});
});

it('should display lib', () => {
expect(element(by.css('p')).getText()).toContain('Amet tempor excepteur occaecat nulla.');
expect(element(by.css('p')).getText()).toContain(
'Amet tempor excepteur occaecat nulla.'
);
});

it('should show `sn-viewport-out` class', () => {
expect(page.getSmallElement().getAttribute('class')).toContain('sn-viewport-out');
it('should show `sn-viewport--out` class', () => {
expect(page.getSmallElement().getAttribute('class')).toContain(
'sn-viewport--out'
);

page.scrollTo(0, 768 / 2);
expect(page.getSmallElement().getAttribute('class')).not.toContain('sn-viewport-out');
expect(page.getSmallElement().getAttribute('class')).not.toContain(
'sn-viewport--out'
);
});

it('should show `sn-viewport-in` class', () => {
it('should show `sn-viewport--in` class', () => {
page.scrollTo(0, 768 / 2);
expect(page.getSmallElement().getAttribute('class')).toContain('sn-viewport-in');
expect(page.getSmallElement().getAttribute('class')).toContain(
'sn-viewport--in'
);

page.scrollTo(0, 0);
expect(page.getSmallElement().getAttribute('class')).not.toContain('sn-viewport-in');
expect(page.getSmallElement().getAttribute('class')).not.toContain(
'sn-viewport--in'
);
});

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

page.scrollTo();
expect(page.getSmallElement().getAttribute('class')).not.toContain('highlight');
expect(page.getSmallElement().getAttribute('class')).not.toContain(
'highlight'
);
});

it('should add `in-viewport` class to large element', () => {
page.scrollTo(0, 768 * 2);
expect(page.getLargeElement().getAttribute('class')).toContain('sn-viewport-in');
expect(page.getLargeElement().getAttribute('class')).toContain(
'sn-viewport--in'
);

page.scrollTo();
expect(page.getLargeElement().getAttribute('class')).not.toContain('sn-viewport-in');
expect(page.getLargeElement().getAttribute('class')).not.toContain(
'sn-viewport--in'
);
});

it('should add `in-viewport` class to element inside a scrollable element', () => {
it('should add `sn-viewport` class to element inside a scrollable element', () => {
page.scrollTo(0, 768 * 3);
expect(page.getInsideScrollableElement().getAttribute('class')).not.toContain('sn-viewport-in');
expect(page.getInsideScrollableElement().getAttribute('class')).toContain('sn-viewport-out');
expect(
page.getScrollableInnerElement().getAttribute('class')
).not.toContain('sn-viewport--in');
expect(page.getScrollableInnerElement().getAttribute('class')).toContain(
'sn-viewport--out'
);

page.scrollableElementScrollTop(768);
expect(page.getInsideScrollableElement().getAttribute('class')).toContain('sn-viewport-in');
expect(page.getInsideScrollableElement().getAttribute('class')).not.toContain('sn-viewport-out');
expect(page.getScrollableInnerElement().getAttribute('class')).toContain(
'sn-viewport--in'
);
expect(
page.getScrollableInnerElement().getAttribute('class')
).not.toContain('sn-viewport--out');

page.scrollableElementScrollTop(768 * 2);
expect(page.getInsideScrollableElement().getAttribute('class')).not.toContain('sn-viewport-in');
expect(page.getInsideScrollableElement().getAttribute('class')).toContain('sn-viewport-out');
expect(
page.getScrollableInnerElement().getAttribute('class')
).not.toContain('sn-viewport--in');
expect(page.getScrollableInnerElement().getAttribute('class')).toContain(
'sn-viewport--out'
);
});

});

0 comments on commit b722d3f

Please sign in to comment.