This repository has been archived by the owner on Jan 6, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 771
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 responsive API to img[src.md], img[src.lt-lg], img[src.gt-xs], etc. * skip actions if responsive keys are not defined * without responsive keys (`src.<alias>`) defined, the ImgSrcDirective should **fall-through** and not change any attributes or properties on the `img` DOM element. The `img.src` attribute is dynamically set only when responsive keys are defined. * defaults to `src=""` if not explicitly assigned * responsive key activation will then assign the activated value to `img.src` attribute. Closes #366, Fixes #81, Fixes #376.
- Loading branch information
1 parent
8b8b595
commit 7390242
Showing
4 changed files
with
353 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,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]; | ||
|
||
} | ||
} | ||
|
||
|
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,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; | ||
} | ||
|
||
} |
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