Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce hooks #365

Merged
merged 17 commits into from
Nov 7, 2018
8 changes: 1 addition & 7 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,11 @@ root = true
[*]
charset = utf-8
indent_style = space
indent_size = 4
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
indent_size = 2

[*.json]
indent_size = 2

[*.md]
insert_final_newline = false
trim_trailing_whitespace = false
65 changes: 56 additions & 9 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Contributions are most welcome! But to avoid unnecessary work, please create an

## Bugs 🐞

When you submit a bug report make sure to include what version of Angular and ng-lazyload-image you are using. Also include what browser the bug occurs in (if relevant), also include what version of `npm` and `node` you are using (if relevant).
When you submit a bugreport make sure to include what version of Angular and ng-lazyload-image you are using. Also include what browser the bug occurs in (if relevant), also include what version of `npm` and `node` you are using (if relevant).
Make it clear if you are using ng-lazyload-image with some other library (or framework) like ionc or material design.

Please try to describe the issue as detailed as possible. If possible, create a simple git repo or provide a [plnkr](https://plnkr.co/) to reproduce the bug. You can fork this one: https://plnkr.co/edit/5pnWgvKLCp7TIoBE69w5
Expand All @@ -25,25 +25,72 @@ Alright, let me give you a introduction to the project.
├── karma.*.js // Config for the unit tests
├── protractor.*.js // Configuration for e2e tests
├── src // The folder that contains all the source files
│   ├── intersection-observer-preset // Logic for intersection observer lazy loading
│   ├── scroll-preset // Logic for scroll-lazy-loading
│   ├── shared-preset // Shared logic between the presets
│   ├── util // Some utility functions
│   ├── hooks-factory.ts // Creates and builds the hooks, that will load the image
│   ├── lazyload-image.directive.ts // The directive declaration
│   ├── lazyload-image.module.ts // The module declaration
│   ├── lazyload-image.ts // Contains logic about when and how the images should be loaded
│   └── scroll-listener.ts // Wrapper for scroll listener
├── test // Contains all unit tests
│   └── types.ts // Contains some share types
├── tsconfig.json
├── tslint.json
├── unit-test.*.js
├── wallaby.js
── webpack.*.js
── webpack.*.js
```

### How does it work?

The project is quite simple. When Angular detects `[lazyLoad]` on a `img` tag it will create a new instance of `LazyLoadImageDirective` ([lazyload-image.directive.ts](src/lazyload-image.directive.ts)). This will trigger `ngAfterContentInit` witch will create a new event listener (or reuse one if there is a existing one for the same scroll target).
The project is quite simple. When the library is initialized, a set of hooks is created. These hooks contains logics about how and when the image should be loaded. The hooks contains of the following functions:
```
getObservable: GetObservableFn<E> // What we should observe
isVisible: IsVisibleFn<E> // Logic to see if the image is visible
loadImage: LoadImageFn // Logic to load the image. It can be async or sync.
setLoadedImage: SetLoadedImageFn // Logic to set the image in DOM
setErrorImage: SetErrorImageFn // Logic to set the error image
setup: SetupFn // Set up function
finally: FinallyFn // Teardown function
```

When Angular detects `[lazyLoad]` on a `img` or `div` tag it will create a new instance of `LazyLoadImageDirective` ([lazyload-image.directive.ts](src/lazyload-image.directive.ts)). This will trigger `ngAfterContentInit` which will emit a new event on `propertyChanges$` and every time a new event is emitted; the setup-hook will be called. After that will the `getObservable`-hook be called which will emit a new event to `isVisible`. If `isVisible` returns true, `loadImage` will be called and then `setLoadedImage` and then `finally`. If something goes wrong (the image can't be loaded or some other errors) the `setErrorImage` will be loaded.

#### Scroll preset

This is the default preset and will check if the image should be loaded on (almost (we are sampling 100 ms)) every scroll event.

`setup` will check if the user has defined a `defaultImage`. If so, we set the `src` attribute to `defaultImage`.

`getObservable` will create a new event listener (or reuse one if there is a existing one for the same scroll target).

Every time `getObservable` emits an event, `isVisible` will check if the images is in the viewport.

If `isVisible` returns true `loadImage` will be called which will load the image, we will also unsubscribe to the scroll events at this point. `loadImage` will return an Observer who will emit a path to the image when it is loaded.

When `loadImage` emits a URL to the image, `setLoadedImage` will be evoked how will insert the image into the DOM.

If `loadImage` emits an error (or something else goes wrong), `setErrorImage` will be called.

And at last will `finally` be called. At this point will the scroll event be unsubscribed, so the footprint will be minimal. However, if the attributes changes, we will restart and call `setup` and every other function once again.

#### Intersection observer preset

This preset will check if the images is loaded by using a intersection observer. This will work like `Scroll preset`, except we will not use any scroll listener.

`setup` will check if the user has defined a `defaultImage`. If so, we set the `src` attribute to `defaultImage`.

`getObservable` will create a new event listener (or reuse one if there is an existing one for the same target).

Every time `getObservable` emits an event, `isVisible` will check if the images is in the viewport.

If `isVisible` returns true `loadImage` will be called which will load the image, we will also unsubscribe to the scroll events at this point. `loadImage` will return an Observer who will emit a path to the image when it is loaded.

When `loadImage` emits an URL to the image, `setLoadedImage` will be evoked how will insert the image into the DOM.

If `loadImage` emits an error (or something else goes wrong), `setErrorImage` will be called.

First of all we check if the user has defined a `defaultImage`, if so we set the `src` attribute to `defaultImage`.
For (almost) every event (we are sampling the events) we check if the image is in viewport. If so we load the image and after the image is loaded we replace the `src` attribute with the value of `[lazyLoad]`. If there was an error while loading the image we try to set the `src` attribute to the value of `errorImage`.
When that is done, we will complete the event stream and unsubscribe to the scroll listener. Which means, as soon the image is loaded the directive will stop do anything.
And at last will `finally` be called. At this point will the image be unobserved by the intersection observer, so the footprint will be minimal. However, if the attributes changes, we will restart and call `setup` and every other function once again.

### Project setup

Expand All @@ -58,4 +105,4 @@ When that is done, we will complete the event stream and unsubscribe to the scro
9. Commit your work
10. Push to your repo
11. [Create a pull request](https://help.github.com/articles/creating-a-pull-request/)
12. Give your self a high five 🖐
12. Give yourself a high five 🖐
192 changes: 190 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Demo: http://tjoskar.github.io/ng-lazyload-image/
### Requirement
The browser you targeting need to have support of `WeakMap` and `String.prototype.includes`. If you need to support an older browser (like IE) you will need to include polyfill for `WeakMap` and `String.prototype.includes` (see https://github.com/zloirock/core-js for example).

If you want to use [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) and want to target Safari and/or IE; you need to include a polyfill: https://github.com/w3c/IntersectionObserver/tree/master/polyfill

### Installation
```
$ npm install ng-lazyload-image --save
Expand All @@ -34,6 +36,29 @@ import { AppComponent } from './app.component';
export class MyAppModule {}
```

`ng-lazyload-image` is using a scroll listener by default but you can use IntersectionObserver if you want instead:

```javascript
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { LazyLoadImageModule, intersectionObserverPreset } from 'ng-lazyload-image';
import { AppComponent } from './app.component';

@NgModule({
declarations: [ AppComponent ],
imports: [
BrowserModule,
LazyLoadImageModule.forRoot({
preset: intersectionObserverPreset
})
],
bootstrap: [ AppComponent ]
})
export class MyAppModule {}
```

See hooks below for more information.

### Usages

```javascript
Expand Down Expand Up @@ -120,12 +145,12 @@ class ImageComponent {
```


You can (from 3.3.0) load image async or change the url on the fly, eg.
You can load image async or change the url on the fly, eg.
```html
<img [lazyLoad]="image$ | async">
```

If you are using Ionic 2 you may need to include your own scroll observable or change the scroll target.
If you are using Ionic 2 and **don't** want to use IntersectionObserver, you may need to include your own scroll observable or change the scroll target.

```javascript
@Component({
Expand Down Expand Up @@ -159,6 +184,169 @@ export class AboutPage {

See example folder for more usages.

### Hooks

It is possible to hook into the loading process by create your own functions.

For example, let's say you want to fetch an image with some custom headers. If so, you can create a custom hook to fetch the image:

```ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { LazyLoadImageModule, intersectionObserverPreset } from 'ng-lazyload-image';
import { AppComponent } from './app.component';

function loadImage({ imagePath }) {
return await fetch(imagePath, {
headers: {
Authorization: 'Bearer ...'
}
}).then(res => res.blob()).then(blob => URL.createObjectURL(blob));
}

@NgModule({
declarations: [ AppComponent ],
imports: [
BrowserModule,
LazyLoadImageModule.forRoot({ loadImage })
],
bootstrap: [ AppComponent ]
})
export class MyAppModule {}
```

The following hooks are supported:

#### getObservable

Should return an observable that emits a new value every time `ng-lazyload-image` should check if the image is in viewport.

Eg.

```ts
// This will trigger an event every second
function getObservable(attributes: Attributes) {
return interval(1000);
}
```

See [intersection-listener.ts](https://github.com/tjoskar/ng-lazyload-image/blob/master/src/intersection-observer-preset/intersection-listener.ts#L19) for example.

#### isVisible

Function to check if the element is vissible.

Eg.
```ts
function isVisible({ event, element, scrollContainer, offset }) {
// `event` is form `getObservable`
return isElementInViewport(element, scrollContainer, offset);
}
```

#### loadImage

Function to load the image. It must return a path to the image (it can however be async, like the example below and/or return a observable).

```ts
function loadImage({ imagePath }) {
return await fetch(imagePath, {
headers: {
Authorization: 'Bearer ...'
}
}).then(res => res.blob()).then(blob => URL.createObjectURL(blob));
}
```

If you don't want to load the image but instead let the browser load it for you, then you can just return the imagePath (We will however not know if the image can't be loaded and the error image will not be used):

```ts
function loadImage({ imagePath }) {
return imagePath;
}
```

#### setLoadedImage

A function to set the image url to the DOM.

Eg.

```ts
function setLoadedImage({ element, imagePath, useSrcset }) {
// `imagePath` comes from `loadImage`
element.src = imagePath;
}
```

#### setErrorImage

This function will be called if the lazy image cant be loaded.

Eg.

```ts
function setErrorImage({ element, errorImagePath, useSrcset }) {
element.src = errorImagePath;
}
```

#### setErrorImage

This function will be called if the lazy image cant be loaded.

Eg.

```ts
function setErrorImage({ element, errorImagePath, useSrcset }) {
element.src = errorImagePath;
}
```

#### setup

This function will be called on setup. Can be usefull for (re)setting css-classes and setting the default image.

This function will be called every time an attrebute is changing.

#### finally

This function will be called on teardown. Can be usefull for setting css-classes.

#### preset

Preset can be usefull when you want to set multible of the functions above.

eg.

```ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { LazyLoadImageModule, intersectionObserverPreset } from 'ng-lazyload-image';
import { AppComponent } from './app.component';

@NgModule({
declarations: [ AppComponent ],
imports: [
BrowserModule,
LazyLoadImageModule.forRoot({
preset: intersectionObserverPreset
})
],
bootstrap: [ AppComponent ]
})
export class MyAppModule {}
```

If you want to use the `intersectionObserverPreset` but overwride on of the functions, you can easily do that:

```ts
LazyLoadImageModule.forRoot({
preset: intersectionObserverPreset,
finally: ({ element }) => console.log('The image is loaded', element)
})
```

### FAQ

**Q** How can I manually trigger the loading of images?
Expand Down
2 changes: 1 addition & 1 deletion dist/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ng-lazyload-image",
"version": "4.0.0",
"version": "4.1.0",
"description": "Lazy image loader for Angular > v2",
"main": "index.js",
"repository": {
Expand Down
4 changes: 3 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { LazyLoadImageDirective } from './src/lazyload-image.directive';
import { LazyLoadImageModule } from './src/lazyload-image.module';
import { intersectionObserverPreset } from './src/intersection-observer-preset';
import { scrollPreset } from './src/scroll-preset';

export { LazyLoadImageDirective, LazyLoadImageModule };
export { LazyLoadImageDirective, LazyLoadImageModule, intersectionObserverPreset, scrollPreset };
export default LazyLoadImageModule;
1 change: 0 additions & 1 deletion karma.default.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ module.exports = config => {
{ pattern: './unit-test.bundle.js', watched: false }
],
preprocessors: {
'./test/helpers/test-helper.ts': ['webpack', 'sourcemap'],
'./unit-test.bundle.js': ['webpack', 'sourcemap']
},

Expand Down
Loading