Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 20 additions & 57 deletions projects/angular-cld/src/lib/cloudinary-image.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -649,18 +649,12 @@ describe('CloudinaryImage', () => {
expect(img.getAttribute('style')).toEqual(jasmine.stringMatching('opacity: 0; position: absolute;'));
});
});

describe('lazy load image', async () => {
describe('cl-image with placeholder and html style', () => {
@Component({
template: `
<div class="startWindow"><cl-image loading="lazy" width="300" public-id="bear"></cl-image></div>
<div style="margin-top: 300px"><cl-image loading="lazy" width="300" public-id="bear"></cl-image></div>
<div style="margin-top: 300px"><cl-image loading="lazy" width="300" public-id="bear"></cl-image></div>
<div style="margin-top: 300px"><cl-image loading="lazy" width="300" public-id="bear"></cl-image></div>
<div style="margin-top: 300px"><cl-image loading="lazy" width="300" public-id="bear"></cl-image></div>
<div style="margin-top: 300px"><cl-image loading="lazy" width="300" public-id="bear"></cl-image></div>
<div style="margin-top: 300px"><cl-image loading="lazy" width="300" public-id="bear"></cl-image></div>
<div class="endWindow" style="margin-top: 300px"><cl-image loading="lazy" width="300" public-id="bear"></cl-image></div>`
template: `<div style="margin-top: 4000px"></div>
<cl-image loading="lazy" public-id="sample" width="500" crop="fit" style="max-height: 100%">
<cl-placeholder type="blur"></cl-placeholder>
</cl-image>`
})
class TestComponent {}

Expand All @@ -670,7 +664,7 @@ describe('CloudinaryImage', () => {
{ cloud_name: '@@fake_angular2_sdk@@', client_hints: true } as CloudinaryConfiguration);
beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [CloudinaryTransformationDirective, CloudinaryImage, TestComponent, LazyLoadDirective],
declarations: [CloudinaryTransformationDirective, CloudinaryImage, TestComponent, CloudinaryPlaceHolder],
providers: [{ provide: Cloudinary, useValue: testLocalCloudinary }]
}).createComponent(TestComponent);

Expand All @@ -679,43 +673,17 @@ describe('CloudinaryImage', () => {
des = fixture.debugElement.queryAll(By.directive(CloudinaryImage));
});

it('should load eagerly', () => {
it('should have style opacity and position when style is passed', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also add a check that the style that was passed exists? (check that we didn't discard the passed value)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alright will update

const img = des[0].children[0].nativeElement as HTMLImageElement;
expect(img.hasAttribute('data-src')).toBe(true);
expect(img.attributes.getNamedItem('data-src').value).toEqual(jasmine.stringMatching('image/upload/bear'));
});
it('Should lazy load post scroll', async() => {
const delay = 300;
const wait = (ms) => new Promise(res => setTimeout(res, ms));
const count = async () => document.querySelectorAll('.startWindow').length;
const scrollDown = async () => {
document.querySelector('.endWindow')
.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'end' });
}

let preCount = 0;
let postCount = 0;
do {
preCount = await count();
await scrollDown();
await wait(delay);
postCount = await count();
} while (postCount > preCount);
await wait(delay);

const img = des[3].children[0].nativeElement as HTMLImageElement;
expect(img.hasAttribute('src')).toBe(true);
expect(img.attributes.getNamedItem('src').value).toEqual(jasmine.stringMatching('image/upload/bear'));
expect(img.getAttribute('style')).toEqual(jasmine.stringMatching('max-height: 100%; opacity: 0; position: absolute;'));
});
});
describe('lazy load image with default placeholder', async () => {
describe('lazy load image', async () => {
@Component({
template: `
<div class="startWindow"><cl-image loading="lazy" width="300" public-id="bear"></cl-image></div>
<div style="margin-top: 300px"><cl-image loading="lazy" width="300" public-id="bear"></cl-image></div>
<div style="margin-top: 300px"><cl-image loading="lazy" width="300" public-id="bear">
<cl-placeholder></cl-placeholder>
</cl-image></div>
<div style="margin-top: 300px"><cl-image loading="lazy" width="300" public-id="bear"></cl-image></div>
<div style="margin-top: 300px"><cl-image loading="lazy" width="300" public-id="bear"></cl-image></div>
<div style="margin-top: 300px"><cl-image loading="lazy" width="300" public-id="bear"></cl-image></div>
<div style="margin-top: 300px"><cl-image loading="lazy" width="300" public-id="bear"></cl-image></div>
Expand All @@ -726,28 +694,25 @@ describe('CloudinaryImage', () => {

let fixture: ComponentFixture<TestComponent>;
let des: DebugElement[]; // the elements w/ the directive
let placeholder: DebugElement[];
let testLocalCloudinary: Cloudinary = new Cloudinary(require('cloudinary-core'),
{ cloud_name: '@@fake_angular2_sdk@@', client_hints: true } as CloudinaryConfiguration);
beforeEach(fakeAsync(() => {
beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [CloudinaryTransformationDirective, CloudinaryImage, TestComponent, LazyLoadDirective, CloudinaryPlaceHolder],
declarations: [CloudinaryTransformationDirective, CloudinaryImage, TestComponent, LazyLoadDirective],
providers: [{ provide: Cloudinary, useValue: testLocalCloudinary }]
}).createComponent(TestComponent);

fixture.detectChanges(); // initial binding
// all elements with an attached CloudinaryImage
des = fixture.debugElement.queryAll(By.directive(CloudinaryImage));
placeholder = fixture.debugElement.queryAll(By.directive(CloudinaryPlaceHolder));
tick();
fixture.detectChanges();
}));
});

it('should load eagerly', () => {
const img = des[0].children[0].nativeElement as HTMLImageElement;
expect(img.hasAttribute('data-src')).toBe(true);
expect(img.attributes.getNamedItem('data-src').value).toEqual(jasmine.stringMatching('image/upload/bear'));
});
it('Should lazy load post scroll', async () => {
it('Should lazy load post scroll', async() => {
const delay = 300;
const wait = (ms) => new Promise(res => setTimeout(res, ms));
const count = async () => document.querySelectorAll('.startWindow').length;
Expand All @@ -766,9 +731,7 @@ describe('CloudinaryImage', () => {
} while (postCount > preCount);
await wait(delay);

const placeholderimg = placeholder[0].children[0].nativeElement as HTMLImageElement;
const img = des[3].children[0].nativeElement as HTMLImageElement;
expect(placeholderimg.attributes.getNamedItem('src').value).toEqual(jasmine.stringMatching('image/upload/e_blur:2000,f_auto,q_1/bear'));
expect(img.hasAttribute('src')).toBe(true);
expect(img.attributes.getNamedItem('src').value).toEqual(jasmine.stringMatching('image/upload/bear'));
});
Expand Down Expand Up @@ -862,7 +825,7 @@ describe('CloudinaryImage', () => {
tick();
fixture.detectChanges();
const img = des[0].children[0].nativeElement as HTMLImageElement;
expect(img.attributes.getNamedItem('src').value).toEqual(jasmine.stringMatching('c_fit,w_30/e_blur:2000,f_auto,q_1/bear'));
expect(img.attributes.getNamedItem('src').value).toEqual(jasmine.stringMatching('c_fit,w_300/e_blur:2000,f_auto,q_1/bear'));
}));
});
describe('placeholder type pixelate', () => {
Expand Down Expand Up @@ -890,7 +853,7 @@ describe('CloudinaryImage', () => {
tick();
fixture.detectChanges();
const img = des[0].children[0].nativeElement as HTMLImageElement;
expect(img.attributes.getNamedItem('src').value).toEqual(jasmine.stringMatching('image/upload/c_fit,w_30/e_pixelate,f_auto,q_1/bear'));
expect(img.attributes.getNamedItem('src').value).toEqual(jasmine.stringMatching('image/upload/c_fit,w_300/e_pixelate,f_auto,q_1/bear'));
}));
});
describe('placeholder type predominant-color with exact dimensions', () => {
Expand Down Expand Up @@ -918,7 +881,7 @@ describe('CloudinaryImage', () => {
tick();
fixture.detectChanges();
const img = des[0].children[0].nativeElement as HTMLImageElement;
expect(img.attributes.getNamedItem('src').value).toEqual(jasmine.stringMatching('image/upload/c_fit,h_30,w_30/ar_1,b_auto,' +
expect(img.attributes.getNamedItem('src').value).toEqual(jasmine.stringMatching('image/upload/c_fit,h_300,w_300/ar_1,b_auto,' +
'c_pad,w_iw_div_2/c_crop,g_north_east,h_1,w_1/f_auto,q_auto/bear'));
}));
});
Expand Down Expand Up @@ -948,7 +911,7 @@ describe('CloudinaryImage', () => {
fixture.detectChanges();
const img = des[0].children[0].nativeElement as HTMLImageElement;
expect(img.attributes.getNamedItem('src').value).toEqual('http://res.cloudinary.com/@@fake_angular2_sdk@@/image/' +
'upload/c_fit,w_30/$currWidth_w,$currHeight_h/ar_1,b_auto,c_pad,w_iw_div_2/c_crop,g_north_east,h_10,w_10/c_fill,h_$currHeight,w_$currWidth/f_auto,q_auto/bear');
'upload/c_fit,w_300/$currWidth_w,$currHeight_h/ar_1,b_auto,c_pad,w_iw_div_2/c_crop,g_north_east,h_10,w_10/c_fill,h_$currHeight,w_$currWidth/f_auto,q_auto/bear');
}));
});
describe('placeholder type vectorize', () => {
Expand Down Expand Up @@ -1004,7 +967,7 @@ describe('CloudinaryImage', () => {
tick();
fixture.detectChanges();
const img = des[0].children[0].nativeElement as HTMLImageElement;
expect(img.attributes.getNamedItem('src').value).toEqual(jasmine.stringMatching('e_sepia/c_fit,w_30/e_blur:2000,f_auto,q_1/bear'));
expect(img.attributes.getNamedItem('src').value).toEqual(jasmine.stringMatching('e_sepia/c_fit,w_300/e_blur:2000,f_auto,q_1/bear'));
}));
});
describe('cl-image with acessibility modes', () => {
Expand Down
31 changes: 17 additions & 14 deletions projects/angular-cld/src/lib/cloudinary-image.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
SimpleChanges,
OnDestroy,
ContentChild,
Renderer2,
} from '@angular/core';
import { Cloudinary } from './cloudinary.service';
import { CloudinaryTransformationDirective } from './cloudinary-transformation.directive';
Expand All @@ -22,9 +23,9 @@ import { accessibilityEffect } from './constants';

@Component({
selector: 'cl-image',
template: `<img [ngStyle]="getPlaceHolderStyle()"(load)="hasLoaded()">
<div *ngIf="placeholderComponent"[style.display]="shouldShowPlaceHolder ? 'inline' : 'none'">
<ng-content></ng-content>
template: `<img (load)="hasLoaded()">
<div *ngIf="placeholderComponent && shouldShowPlaceHolder" [style.display]="shouldShowPlaceHolder ? 'inline' : 'none'">
<ng-content></ng-content>
</div>
`,
})
Expand All @@ -35,6 +36,7 @@ export class CloudinaryImage
@Input('loading') loading: string;
@Input('width') width?: string;
@Input('height') height?: string;

@Input('accessibility') accessibility?: string;

@ContentChildren(CloudinaryTransformationDirective)
Expand All @@ -48,7 +50,7 @@ export class CloudinaryImage
shouldShowPlaceHolder = true;
options: object = {};

constructor(private el: ElementRef, private cloudinary: Cloudinary) {}
constructor(private el: ElementRef, private cloudinary: Cloudinary, private renderer: Renderer2) {}

ngOnInit(): void {
if (isBrowser()) {
Expand Down Expand Up @@ -94,9 +96,11 @@ export class CloudinaryImage
}
}

getPlaceHolderStyle() {
return {[this.shouldShowPlaceHolder ? 'opacity' : ''] : '0',
[this.shouldShowPlaceHolder ? 'position' : ''] : 'absolute'}
setPlaceHolderStyle() {
if (this.placeholderComponent) {
this.renderer.setStyle(this.el.nativeElement.children[0], 'opacity', '0' );
this.renderer.setStyle(this.el.nativeElement.children[0], 'position', 'absolute' );
}
}

hasLoaded() {
Expand Down Expand Up @@ -138,7 +142,6 @@ export class CloudinaryImage
options.src = this.accessibilityModeHandler();
}
const imageTag = this.cloudinary.imageTag(this.publicId, options);

this.setElementAttributes(image, imageTag.attributes());
if (options.responsive) {
this.cloudinary.responsive(image, options);
Expand All @@ -149,8 +152,13 @@ export class CloudinaryImage
setElementAttributes(element, attributesLiteral) {
Object.keys(attributesLiteral).forEach(attrName => {
const attr = attrName === 'src' && this.loading === 'lazy' ? 'data-src' : attrName;
element.setAttribute(attr, attributesLiteral[attrName]);
this.renderer.setAttribute(element, attr, attributesLiteral[attrName]);
});

// Enforcing placeholder style
if (this.shouldShowPlaceHolder) {
this.setPlaceHolderStyle();
}
}

/**
Expand All @@ -163,13 +171,8 @@ export class CloudinaryImage
if (placeholderOptions['width']) {
if (placeholderOptions['width'] === 'auto') {
placeholderOptions['width'] = image.getAttribute('data-width');
} else if (this.placeholderComponent.type !== 'vectorize') {
placeholderOptions['width'] = Math.ceil(parseInt(options['width'], 10) * 0.1);
}
}
if (placeholderOptions['height']) {
placeholderOptions['height'] = Math.ceil(parseInt(options['height'], 10) * 0.1);
}
this.placeholderComponent.options = placeholderOptions;
}

Expand Down
16 changes: 14 additions & 2 deletions projects/angular-cld/src/lib/cloudinary-placeholder.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import {
Component,
HostBinding,
Input,
ElementRef,
Renderer2,
} from '@angular/core';
import {Cloudinary} from './cloudinary.service';
import { placeholderImageOptions, predominantColorTransformPxl } from './constants';

@Component({
selector: 'cl-placeholder',
template: `<img [src]="this.placeholderImg" [style.width.px]="this.itemWidth" [style.height.px]="this.itemHeight">`
template: `<img [src]="this.placeholderImg">`
,
})
export class CloudinaryPlaceHolder implements AfterContentChecked {
Expand All @@ -21,7 +23,7 @@ export class CloudinaryPlaceHolder implements AfterContentChecked {
options: object = {};
placeholderImg: string;

constructor(private cloudinary: Cloudinary) {}
constructor(private cloudinary: Cloudinary, private renderer: Renderer2, private el: ElementRef) {}

setWidth(width) {
this.itemWidth = width;
Expand All @@ -36,6 +38,8 @@ export class CloudinaryPlaceHolder implements AfterContentChecked {
}

ngAfterContentChecked() {
const imageTag = this.cloudinary.imageTag(this.publicId, this.options);
this.setElementAttributes(this.el.nativeElement.children[0], imageTag.attributes());
this.placeholderImg = this.getPlaceholderImage();
}

Expand All @@ -46,4 +50,12 @@ export class CloudinaryPlaceHolder implements AfterContentChecked {
return this.cloudinary.url(this.publicId, {transformation: [this.options, ...(placeholderImageOptions[this.type] || placeholderImageOptions['blur'])]});
}
}

setElementAttributes(element, attributesLiteral) {
Object.keys(attributesLiteral).forEach(attrName => {
if (attrName !== 'src') {
this.renderer.setAttribute(element, attrName, attributesLiteral[attrName]);
}
});
}
}