Skip to content

Commit

Permalink
feat(viewport): added new Viewport module
Browse files Browse the repository at this point in the history
viewport helps to check if an element is in viewport or not
  • Loading branch information
xmlking committed Dec 2, 2018
1 parent bf290bf commit d2f1a2e
Show file tree
Hide file tree
Showing 26 changed files with 1,128 additions and 225 deletions.
252 changes: 125 additions & 127 deletions PLAYBOOK.md

Large diffs are not rendered by default.

362 changes: 282 additions & 80 deletions angular.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions apps/webapp/src/polyfills.ts
Expand Up @@ -82,3 +82,6 @@ import 'zone.js/dist/zone'; // Included with Angular CLI.
// Add global to window, assigning the value of window itself.
(window as any).global = window;
import 'core-js/es7/array';

/* Polyfill for IntersectionObserver */
import 'intersection-observer';
13 changes: 9 additions & 4 deletions libs/core/src/lib/menu-data.ts
Expand Up @@ -34,17 +34,17 @@ export const defaultMenu: MenuItem[] = [
},
{
name: 'Experiments',
icon: 'pie_chart_outlined',
icon: 'developer_board',
disabled: false,
children: [
{
name: 'Animations',
icon: 'view_list',
icon: 'screen_rotation',
link: '/dashboard/experiments/animations',
},
{
name: 'Upload',
icon: 'directions',
icon: 'backup',
link: '/dashboard/experiments/file-upload',
},
{
Expand All @@ -64,14 +64,19 @@ export const defaultMenu: MenuItem[] = [
},
{
name: 'Knob',
icon: 'directions',
icon: 'rotate_right',
link: '/dashboard/experiments/knob',
},
{
name: 'Layout',
icon: 'apps',
link: '/dashboard/experiments/layout',
},
{
name: 'Viewport',
icon: 'view_carousel',
link: '/dashboard/experiments/viewport',
},
// {
// name: 'Microinteractions',
// icon: 'casino',
Expand Down
2 changes: 2 additions & 0 deletions libs/core/src/lib/services/feature.service.ts
Expand Up @@ -22,6 +22,7 @@ export enum BrowserFeatureKey {
WebBluetoothAPI = 'Web Bluetooth',
SpeechSynthesis = 'Speech Synthesis',
SpeechRecognition = 'Speech Recognition',
IntersectionObserver = 'Intersection Observer',
}

export class BrowserFeature {
Expand Down Expand Up @@ -63,6 +64,7 @@ export class FeatureService {
'mozSpeechRecognition' in this.window ||
'msSpeechRecognition' in this.window ||
'oSpeechRecognition' in this.window,
[BrowserFeatureKey.IntersectionObserver]: 'IntersectionObserver' in this.window,
};
}

Expand Down
Expand Up @@ -13,6 +13,15 @@
<div fxFlex="0 1 calc(33.3% - 32px)" fxFlex.lt-md="0 1 calc(50% - 32px)" fxFlex.lt-sm="100%">
<mat-card><a routerLink="/dashboard/experiments/animations">Animations</a></mat-card>
</div>
<div fxFlex="0 1 calc(33.3% - 32px)" fxFlex.lt-md="0 1 calc(50% - 32px)" fxFlex.lt-sm="100%">
<mat-card><a routerLink="/dashboard/experiments/viewport">Viewport</a></mat-card>
</div>
<div fxFlex="0 1 calc(33.3% - 32px)" fxFlex.lt-md="0 1 calc(50% - 32px)" fxFlex.lt-sm="100%">
<mat-card><a routerLink="/dashboard/experiments/knob">Knob</a></mat-card>
</div>
<div fxFlex="0 1 calc(33.3% - 32px)" fxFlex.lt-md="0 1 calc(50% - 32px)" fxFlex.lt-sm="100%">
<mat-card><a routerLink="/dashboard/experiments/clap">Micro Interactions</a></mat-card>
</div>
<div fxFlex="0 1 calc(33.3% - 32px)" fxFlex.lt-md="0 1 calc(50% - 32px)" fxFlex.lt-sm="100%">
<mat-card><a routerLink="/dashboard/widgets">Widgets</a></mat-card>
</div>
Expand Down
@@ -0,0 +1,38 @@
<div class="static-header">
<h2>Scroll to load more images</h2>
<p>
Total images displayed: <strong>{{ totalImagesShown }}</strong>
</p>
<small>Picture courtesy: <a href="https://unsplash.com/">Unsplash</a></small>
</div>
<div class="images">
<div
class="image-container"
*ngFor="let image of imageItemCollection"
[oneTime]="true"
(inViewport)="show($event, image)"
>
<ng-container *ngIf="image.show"> <img name="beautiful-image" src="{{ image.url }}" /> </ng-container>
</div>
</div>
<!-- Animation demo -->
<div>
<div class="images">
<img
(inViewport)="anim($event, 'cowboy')"
src="https://d33wubrfki0l68.cloudfront.net/87e1bf9b49ec8a0ae7b343fce4f95fba32eb2cf8/0a3e5/images/cowboy.svg"
width="275"
height="275"
class="animate-me cowboy"
/>
</div>
<div class="images">
<img
(inViewport)="anim($event, 'chef')"
src="https://d33wubrfki0l68.cloudfront.net/185c274053d2803726663cad3aac7245f90aa3fe/7df55/images/chef.svg"
width="275"
height="275"
class="animate-me chef"
/>
</div>
</div>
@@ -0,0 +1,59 @@
a {
text-decoration: none;
}

.static-header {
position: fixed;
z-index: 1000;
background: #eee;
text-align: center;
width: 100%;

padding: 10px;
}

.images {
padding-top: 110px;
left: 0;
right: 0;
text-align: center;
}

.images img {
max-width: 500px;
}

.images .image-container, .images img {
height: 350px;
}

// animation demo

.cowboy.fancy {
animation: anim1 .7s ease-out;
}
.chef.fancy {
animation: anim2 .7s ease-out;
}

@keyframes anim1 {
0% {
opacity: 0;
transform: translateX(-30rem) rotate(-45deg);
}
100% {
opacity: 1;
transform: scale(1) rotate(0deg);
}
}

@keyframes anim2 {
0% {
opacity: 0;
transform: translateX(30rem) rotate(45deg);
}
100% {
opacity: 1;
transform: scale(1) rotate(0deg);
}
}
99 changes: 99 additions & 0 deletions libs/experiments/src/lib/containers/viewport/viewport.component.ts
@@ -0,0 +1,99 @@
import { Component, OnInit } from '@angular/core';

interface ImageItem {
id: number;
url: string;
show: boolean;
}
@Component({
selector: 'ngx-viewport',
templateUrl: './viewport.component.html',
styleUrls: ['./viewport.component.scss'],
})
export class ViewportComponent implements OnInit {
// prettier-ignore
// tslint:disable:max-line-length
imageItemCollection: ImageItem[] = [
{
id: 1,
url: 'https://images.unsplash.com/photo-1512672378591-74fbb56b1d28?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=111881731843c98860fd6ede341337d7&auto=format&fit=crop&w=1350&q=80',
show: false,
},
{
id: 2,
url: 'https://images.unsplash.com/photo-1486495939893-f384c2860f55?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=bf36a4694839666ab094bcdd0bb88651&auto=format&fit=crop&w=1350&q=80',
show: false,
},
{
id: 3,
url: 'https://images.unsplash.com/photo-1514913274516-4aa04f176f8c?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=a6940b0c53d64fc564bed31bb6aa8d9b&auto=format&fit=crop&w=1760&q=80',
show: false,
},
{
id: 4,
url: 'https://images.unsplash.com/photo-1523286877159-d9636545890c?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=f44497f72d77b9e8e27e87521e025edc&auto=format&fit=crop&w=1351&q=80',
show: false,
},
{
id: 5,
url: 'https://images.unsplash.com/photo-1459886757952-87e191b82aeb?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=6c977d9f0c074c220a31f1e89449c3aa&auto=format&fit=crop&w=1350&q=80',
show: false,
},
{
id: 6,
url: 'https://images.unsplash.com/photo-1519423961530-9131478718db?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=e6132d79c5060ba00caa99cf39457da6&auto=format&fit=crop&w=1350&q=80',
show: false,
},
{
id: 7,
url: 'https://images.unsplash.com/photo-1482510356941-d087154c2931?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=cd6c067c548407960ec92f1e820775ee&auto=format&fit=crop&w=1355&q=80',
show: false,
},
{
id: 8,
url: 'https://images.unsplash.com/photo-1520507215037-061ed0f37178?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=0b0ee4f4dcd684859da448cc26c707a2&auto=format&fit=crop&w=1350&q=80',
show: false,
},
{
id: 9,
url: 'https://images.unsplash.com/photo-1522447984233-657d56c465d8?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=b2efa4e73b38094995897590487ba5b4&auto=format&fit=crop&w=1350&q=80',
show: false,
},
];

constructor() {}

ngOnInit() {}

get totalImagesShown(): number {
return (this.imageItemCollection.filter(imageItem => imageItem.show) || []).length;
}

show(event: Partial<IntersectionObserverEntry>, image: ImageItem) {
if (event.intersectionRatio >= 0.5) {
image.show = true;
console.log(
`in-view ==> image: ${image.id}, intersectionRatio: ${event.intersectionRatio}, isIntersecting: ${
event.isIntersecting
}`,
);
} else {
console.log(
`out-of-view <== image: ${image.id}, intersectionRatio: ${event.intersectionRatio}, isIntersecting: ${
event.isIntersecting
}`,
);
}
}

anim(event: Partial<IntersectionObserverEntry>, element: string) {
console.log(
`anim: ${element} intersectionRatio: ${event.intersectionRatio}, isIntersecting: ${event.isIntersecting}`,
);
if (event.intersectionRatio >= 0.5) {
event.target.classList.add('fancy');
} else {
event.target.classList.remove('fancy');
}
}
}
13 changes: 11 additions & 2 deletions libs/experiments/src/lib/experiments.module.ts
Expand Up @@ -3,6 +3,7 @@ import { RouterModule } from '@angular/router';
import { FilePondModule, registerPlugin } from 'ngx-filepond';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { KnobModule } from '@xmlking/ngx-knob';
import { InViewportModule } from '@ngx-starter-kit/ngx-utils';

import { ClapModule } from '@ngx-starter-kit/clap';
import { LedModule } from '@ngx-starter-kit/led';
Expand All @@ -19,13 +20,14 @@ import { VirtualScrollComponent } from './containers/virtual-scroll/virtual-scro
import { StickyTableComponent } from './containers/sticky-table/sticky-table.component';
import { LedDemoComponent } from './containers/led-demo/led-demo.component';
import { ImageCompComponent } from './containers/image-comp/image-comp.component';
import { LayoutComponent } from './containers/layout/layout.component';
import { CardComponent } from './components/card/card.component';
import { ViewportComponent } from './containers/viewport/viewport.component';

// Registering plugins
import * as FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type';
import * as FilepondPluginFileValidateSize from 'filepond-plugin-file-validate-size';
import * as FilepondPluginImagePreview from 'filepond-plugin-image-preview';
import { LayoutComponent } from './containers/layout/layout.component';
import { CardComponent } from './components/card/card.component';

registerPlugin(FilePondPluginFileValidateType, FilepondPluginFileValidateSize, FilepondPluginImagePreview);

Expand All @@ -38,6 +40,7 @@ registerPlugin(FilePondPluginFileValidateType, FilepondPluginFileValidateSize, F
ClapModule,
LedModule,
KnobModule,
InViewportModule,
ImageComparisonModule,
RouterModule.forChild([
/* {path: '', pathMatch: 'full', component: InsertYourComponentHere} */
Expand Down Expand Up @@ -92,6 +95,11 @@ registerPlugin(FilePondPluginFileValidateType, FilepondPluginFileValidateSize, F
component: LayoutComponent,
data: { title: 'Layout', depth: 3 },
},
{
path: 'viewport',
component: ViewportComponent,
data: { title: 'Viewport', depth: 3 },
},
]),
],
declarations: [
Expand All @@ -107,6 +115,7 @@ registerPlugin(FilePondPluginFileValidateType, FilepondPluginFileValidateSize, F
ImageCompComponent,
LayoutComponent,
CardComponent,
ViewportComponent,
],
})
export class ExperimentsModule {}
57 changes: 57 additions & 0 deletions libs/ngx-utils/README.md
Expand Up @@ -3,3 +3,60 @@
same as [@ngrx-utils/store](https://github.com/ngrx-utils/ngrx-utils) without dependency on `@ngrx/store`

**Pipes:** filter, group-by, safeHtml

**Directive:** inViewport, ngLet, routerLinkMatch

### InViewport Directive

> Add `IntersectionObserver` [polyfill](https://github.com/w3c/IntersectionObserver/tree/master/polyfill) to [polyfills.ts](../../apps/webapp/src/polyfills.ts) for `Safari` Support
```html
<div ngxInViewport (inViewport)="showMyElement=true">
<ng-container *ngIf="showMyElement"> <div>Hello World!</div> </ng-container>
</div>
```

> If `entry.intersectionRatio >= 0.5` ==> `Inside Viewport`
> <br/>
> If `entry.intersectionRatio < 0.5` ==> `Outside Viewport`
#### Flags
1. Trigger only One Time : `[oneTime]="true"` usecase: image loading.
2. Server-Side Rendering : By default, loads the elements on the server.
> If you do not want to pre-render the elements in server, you can set `preRender to false. i.e., `[preRender]="false"`
### Viewport Service

> You can use `ViewportService` itself in any Component
```typescript
import { ElementRef, OnDestroy, OnInit } from '@angular/core';
import { untilDestroy, ViewportService } from '@ngx-starter-kit/ngx-utils';

export class ViewportDemoComponent implements OnInit, OnDestroy {
public constructor(private element: ElementRef, private viewportService: ViewportService) {}

public ngOnInit(): void {
this.viewportService
.observe(this.element.nativeElement)
.pipe(untilDestroy(this))
.subscribe((entry: IntersectionObserverEntry) => {
if (entry.isIntersecting) {
this.show();
} else {
this.hide();
}
});
}

ngOnDestroy() {}

private show(): void {
// => Animation
}

private hide(): void {
// <= Animation
}
}
```

0 comments on commit d2f1a2e

Please sign in to comment.