-
Notifications
You must be signed in to change notification settings - Fork 773
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(api): add responsive API for img elements
* add ImgSrcsetDirective * to inject <source> elements to support responsive images * Inject a <source> element for every srcset.<breakpoint alias> in the HTML markup of an <img> element contained in a <picture> elemen * support usages without `<picture>` parents * add responsive API to img.src: src.md, src.lt-lg, src.gt-xs, etc. * repackage API classes to easily distinguish flexbox APIs and extended responsive APIs Closes #366, Fixes #81, Fixes #376.
- Loading branch information
1 parent
64a7c50
commit 6d82622
Showing
4 changed files
with
354 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,216 @@ | ||
/** | ||
* @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' | ||
}); | ||
}); | ||
|
||
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]; | ||
|
||
} | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
/** | ||
* @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 {ɵgetDOM as getDom} from '@angular/platform-browser'; | ||
|
||
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], | ||
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); | ||
} | ||
|
||
/** | ||
* Listen for responsive changes to update the img.src attribute | ||
*/ | ||
ngOnInit() { | ||
super.ngOnInit(); | ||
|
||
// Cache initial value of `src` to use as responsive fallback | ||
this.cacheDefaultSrc(this.defaultSrc); | ||
|
||
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._mqActivation ? this._mqActivation.activatedInput || '' : this.defaultSrc; | ||
this._renderer.setAttribute(this.nativeElement, 'src', 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) { | ||
const currentVal = this._queryInput('src'); | ||
if (typeof currentVal === 'undefined') { | ||
this._cacheInput('src', value || ''); | ||
} | ||
} | ||
|
||
/** | ||
* Empty values are maintained, undefined values are exposed as '' | ||
*/ | ||
protected get defaultSrc(): string { | ||
return this._queryInput('src') || | ||
getDom().getAttribute(this.nativeElement, '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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters