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

feat(api): add responsive API for img elements #384

Merged
merged 1 commit into from
Sep 7, 2017
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
222 changes: 222 additions & 0 deletions src/lib/api/ext/img-src.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component} from '@angular/core';
import {CommonModule} from '@angular/common';
import {ComponentFixture, TestBed, inject} from '@angular/core/testing';

import {DEFAULT_BREAKPOINTS_PROVIDER} from '../../media-query/breakpoints/break-points-provider';
import {BreakPointRegistry} from '../../media-query/breakpoints/break-point-registry';
import {MockMatchMedia} from '../../media-query/mock/mock-match-media';
import {MatchMedia} from '../../media-query/match-media';
import {FlexLayoutModule} from '../../module';

import {customMatchers} from '../../utils/testing/custom-matchers';
import {makeCreateTestComponent, queryFor} from '../../utils/testing/helpers';
import {expect} from '../../utils/testing/custom-matchers';
import {_dom as _} from '../../utils/testing/dom-tools';

const SRC_URLS = {
'xs': [
'https://dummyimage.com/300x200/c7751e/fff.png',
'https://dummyimage.com/300x200/c7751e/000.png'
],
'gt-xs': [
'https://dummyimage.com/400x250/c7c224/fff.png',
'https://dummyimage.com/400x250/c7c224/000.png'
],
'md': [
'https://dummyimage.com/500x300/76c720/fff.png',
'https://dummyimage.com/500x300/76c720/000.png'
],
'lt-lg': [
'https://dummyimage.com/600x350/25c794/fff.png',
'https://dummyimage.com/600x350/25c794/000.png'
],
'lg': [
'https://dummyimage.com/700x400/258cc7/fff.png',
'https://dummyimage.com/700x400/258cc7/000.png'
],
'lt-xl': [
'https://dummyimage.com/800x500/b925c7/ffffff.png',
'https://dummyimage.com/800x500/b925c7/000.png'
]
};
const DEFAULT_SRC = 'https://dummyimage.com/300x300/c72538/ffffff.png';

describe('img-src directive', () => {
let fixture: ComponentFixture<any>;
let matchMedia: MockMatchMedia;
let breakpoints: BreakPointRegistry;

let componentWithTemplate = (template: string) => {
fixture = makeCreateTestComponent(() => TestSrcComponent)(template);

inject([MatchMedia, BreakPointRegistry],
(_matchMedia: MockMatchMedia, _breakpoints: BreakPointRegistry) => {
matchMedia = _matchMedia;
breakpoints = _breakpoints;
})();
};

beforeEach(() => {
jasmine.addMatchers(customMatchers);

// Configure testbed to prepare services
TestBed.configureTestingModule({
imports: [CommonModule, FlexLayoutModule],
declarations: [TestSrcComponent],
providers: [
BreakPointRegistry, DEFAULT_BREAKPOINTS_PROVIDER,
{provide: MatchMedia, useClass: MockMatchMedia}
]
});
});

describe('with static api', () => {
it('should preserve the static src attribute', () => {
let url = 'https://dummyimage.com/300x300/c72538/ffffff.png';
componentWithTemplate(`
<img src="${url}">
`);
const img = queryFor(fixture, 'img')[0].nativeElement;

fixture.detectChanges();
expect(_.getAttribute( img, 'src')).toEqual(url);
});

it('should work with empty src attributes', () => {
componentWithTemplate(`
<img src="">
`);
const img = queryFor(fixture, 'img')[0].nativeElement;

fixture.detectChanges();
expect(img).toHaveAttributes({
src: ''
});
});

it('should work standard input bindings', () => {
componentWithTemplate(`
<img [src]="defaultSrc" [src.xs]="xsSrc">
`);
const img = queryFor(fixture, 'img')[0].nativeElement;

fixture.detectChanges();
expect(img).toHaveAttributes({
src: 'https://dummyimage.com/300x300/c72538/ffffff.png'
});

let url = 'https://dummyimage.com/700x400/258cc7/fff.png';
fixture.componentInstance.defaultSrc = url;
fixture.detectChanges();
expect(img).toHaveAttributes({ src: url });

});

it('should work when `src` value is not defined', () => {
componentWithTemplate(`
<img src >
`);

const img = queryFor(fixture, 'img')[0].nativeElement;
fixture.detectChanges();
expect(img).toHaveAttributes({
src: ''
});
});

it('should only work with "<img>" elements.', () => {
componentWithTemplate(`
<iframe src.xs="none.png" >
`);

const img = queryFor(fixture, 'iframe')[0].nativeElement;
fixture.detectChanges();
expect(img).not.toHaveAttributes({
src: ''
});
});

});

describe('with responsive api', () => {

it('should work with a isolated image element and responsive srcs', () => {
componentWithTemplate(`
<img [src]="xsSrc"
[src.md]="mdSrc">
`);
fixture.detectChanges();

let img = queryFor(fixture, 'img')[0].nativeElement;

matchMedia.activate('md');
fixture.detectChanges();
expect(img).toBeDefined();
expect(img).toHaveAttributes({
src: SRC_URLS['md'][0]
});

// When activating an unused breakpoint, fallback to default [src] value
matchMedia.activate('xl');
fixture.detectChanges();
expect(img).toHaveAttributes({
src: SRC_URLS['xs'][0]
});
});

it('should work if default [src] is not defined', () => {
componentWithTemplate(`
<img [src.md]="mdSrc">
`);
fixture.detectChanges();
matchMedia.activate('md');
fixture.detectChanges();

let img = queryFor(fixture, 'img')[0].nativeElement;
expect(img).toBeDefined();
expect(img).toHaveAttributes({
src: SRC_URLS['md'][0]
});

// When activating an unused breakpoint, fallback to default [src] value
matchMedia.activate('xl');
fixture.detectChanges();
expect(img).toHaveAttributes({
src: ''
});
});

});
});

// *****************************************************************
// Template Component
// *****************************************************************

@Component({
selector: 'test-src-api',
template: ''
})
export class TestSrcComponent {
defaultSrc = '';
xsSrc = '';
mdSrc = '';
lgSrc = '';

constructor() {
this.defaultSrc = DEFAULT_SRC;
this.xsSrc = SRC_URLS['xs'][0];
this.mdSrc = SRC_URLS['md'][0];
this.lgSrc = SRC_URLS['lg'][0];

}
}


127 changes: 127 additions & 0 deletions src/lib/api/ext/img-src.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {
Directive,
ElementRef,
Input,
OnInit,
OnChanges,
Renderer2
} from '@angular/core';

import {BaseFxDirective} from '../core/base';
import {MediaMonitor} from '../../media-query/media-monitor';

/**
* This directive provides a responsive API for the HTML <img> 'src' attribute
* and will update the img.src property upon each responsive activation.
*
* e.g.
* <img src="defaultScene.jpg" src.xs="mobileScene.jpg"></img>
*
* @see https://css-tricks.com/responsive-images-youre-just-changing-resolutions-use-src/
*/
@Directive({
selector: `
img[src.xs], img[src.sm], img[src.md], img[src.lg], img[src.xl],
img[src.lt-sm], img[src.lt-md], img[src.lt-lg], img[src.lt-xl],
img[src.gt-xs], img[src.gt-sm], img[src.gt-md], img[src.gt-lg]
`
})
export class ImgSrcDirective extends BaseFxDirective implements OnInit, OnChanges {

/* tslint:disable */
@Input('src') set srcBase(val) { this.cacheDefaultSrc(val); }

@Input('src.xs') set srcXs(val) { this._cacheInput('srcXs', val); }
@Input('src.sm') set srcSm(val) { this._cacheInput('srcSm', val); }
@Input('src.md') set srcMd(val) { this._cacheInput('srcMd', val); }
@Input('src.lg') set srcLg(val) { this._cacheInput('srcLg', val); }
@Input('src.xl') set srcXl(val) { this._cacheInput('srcXl', val); }

@Input('src.lt-sm') set srcLtSm(val) { this._cacheInput('srcLtSm', val); }
@Input('src.lt-md') set srcLtMd(val) { this._cacheInput('srcLtMd', val); }
@Input('src.lt-lg') set srcLtLg(val) { this._cacheInput('srcLtLg', val); }
@Input('src.lt-xl') set srcLtXl(val) { this._cacheInput('srcLtXl', val); }

@Input('src.gt-xs') set srcGtXs(val) { this._cacheInput('srcGtXs', val); }
@Input('src.gt-sm') set srcGtSm(val) { this._cacheInput('srcGtSm', val); }
@Input('src.gt-md') set srcGtMd(val) { this._cacheInput('srcGtMd', val); }
@Input('src.gt-lg') set srcGtLg(val) { this._cacheInput('srcGtLg', val); }
/* tslint:enable */

constructor(elRef: ElementRef, renderer: Renderer2, monitor: MediaMonitor) {
super(monitor, elRef, renderer);
this._cacheInput('src', elRef.nativeElement.getAttribute('src') || '');
}

/**
* Listen for responsive changes to update the img.src attribute
*/
ngOnInit() {
super.ngOnInit();

if (this.hasResponsiveKeys) {
// Listen for responsive changes
this._listenForMediaQueryChanges('src', this.defaultSrc, () => {
this._updateSrcFor();
});
}
this._updateSrcFor();
}

/**
* Update the 'src' property of the host <img> element
*/
ngOnChanges() {
if (this.hasInitialized) {
this._updateSrcFor();
}
}

/**
* Use the [responsively] activated input value to update
* the host img src attribute or assign a default `img.src=''`
* if the src has not been defined.
*
* Do nothing to standard `<img src="">` usages, only when responsive
* keys are present do we actually call `setAttribute()`
*/
protected _updateSrcFor() {
if (this.hasResponsiveKeys) {
let url = this.activatedValue || this.defaultSrc;
this._renderer.setAttribute(this.nativeElement, 'src', String(url));
}
}

/**
* Cache initial value of 'src', this will be used as fallback when breakpoint
* activations change.
* NOTE: The default 'src' property is not bound using @Input(), so perform
* a post-ngOnInit() lookup of the default src value (if any).
*/
protected cacheDefaultSrc(value?: string) {
this._cacheInput('src', value || '');
}

/**
* Empty values are maintained, undefined values are exposed as ''
*/
protected get defaultSrc(): string {
return this._queryInput('src') || '';
}

/**
* Does the <img> have 1 or more src.<xxx> responsive inputs
* defined... these will be mapped to activated breakpoints.
*/
protected get hasResponsiveKeys() {
return Object.keys(this._inputMap).length > 1;
}

}
1 change: 1 addition & 0 deletions src/lib/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ export * from './flexbox/flex-order';
export * from './ext/class';
export * from './ext/style';
export * from './ext/show-hide';
export * from './ext/img-src';

4 changes: 3 additions & 1 deletion src/lib/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {LayoutGapDirective} from './api/flexbox/layout-gap';
import {ShowHideDirective} from './api/ext/show-hide';
import {ClassDirective} from './api/ext/class';
import {StyleDirective} from './api/ext/style';
import {ImgSrcDirective} from './api/ext/img-src';

/**
* Since the equivalent results are easily achieved with a css class attached to each
Expand All @@ -52,7 +53,8 @@ const ALL_DIRECTIVES = [
FlexAlignDirective,
ShowHideDirective,
ClassDirective,
StyleDirective
StyleDirective,
ImgSrcDirective
];

/**
Expand Down