Skip to content
This repository was archived by the owner on Jan 6, 2025. It is now read-only.

Commit 7390242

Browse files
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.
1 parent 8b8b595 commit 7390242

File tree

4 files changed

+353
-1
lines changed

4 files changed

+353
-1
lines changed

src/lib/api/ext/img-src.spec.ts

+222
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import {Component} from '@angular/core';
9+
import {CommonModule} from '@angular/common';
10+
import {ComponentFixture, TestBed, inject} from '@angular/core/testing';
11+
12+
import {DEFAULT_BREAKPOINTS_PROVIDER} from '../../media-query/breakpoints/break-points-provider';
13+
import {BreakPointRegistry} from '../../media-query/breakpoints/break-point-registry';
14+
import {MockMatchMedia} from '../../media-query/mock/mock-match-media';
15+
import {MatchMedia} from '../../media-query/match-media';
16+
import {FlexLayoutModule} from '../../module';
17+
18+
import {customMatchers} from '../../utils/testing/custom-matchers';
19+
import {makeCreateTestComponent, queryFor} from '../../utils/testing/helpers';
20+
import {expect} from '../../utils/testing/custom-matchers';
21+
import {_dom as _} from '../../utils/testing/dom-tools';
22+
23+
const SRC_URLS = {
24+
'xs': [
25+
'https://dummyimage.com/300x200/c7751e/fff.png',
26+
'https://dummyimage.com/300x200/c7751e/000.png'
27+
],
28+
'gt-xs': [
29+
'https://dummyimage.com/400x250/c7c224/fff.png',
30+
'https://dummyimage.com/400x250/c7c224/000.png'
31+
],
32+
'md': [
33+
'https://dummyimage.com/500x300/76c720/fff.png',
34+
'https://dummyimage.com/500x300/76c720/000.png'
35+
],
36+
'lt-lg': [
37+
'https://dummyimage.com/600x350/25c794/fff.png',
38+
'https://dummyimage.com/600x350/25c794/000.png'
39+
],
40+
'lg': [
41+
'https://dummyimage.com/700x400/258cc7/fff.png',
42+
'https://dummyimage.com/700x400/258cc7/000.png'
43+
],
44+
'lt-xl': [
45+
'https://dummyimage.com/800x500/b925c7/ffffff.png',
46+
'https://dummyimage.com/800x500/b925c7/000.png'
47+
]
48+
};
49+
const DEFAULT_SRC = 'https://dummyimage.com/300x300/c72538/ffffff.png';
50+
51+
describe('img-src directive', () => {
52+
let fixture: ComponentFixture<any>;
53+
let matchMedia: MockMatchMedia;
54+
let breakpoints: BreakPointRegistry;
55+
56+
let componentWithTemplate = (template: string) => {
57+
fixture = makeCreateTestComponent(() => TestSrcComponent)(template);
58+
59+
inject([MatchMedia, BreakPointRegistry],
60+
(_matchMedia: MockMatchMedia, _breakpoints: BreakPointRegistry) => {
61+
matchMedia = _matchMedia;
62+
breakpoints = _breakpoints;
63+
})();
64+
};
65+
66+
beforeEach(() => {
67+
jasmine.addMatchers(customMatchers);
68+
69+
// Configure testbed to prepare services
70+
TestBed.configureTestingModule({
71+
imports: [CommonModule, FlexLayoutModule],
72+
declarations: [TestSrcComponent],
73+
providers: [
74+
BreakPointRegistry, DEFAULT_BREAKPOINTS_PROVIDER,
75+
{provide: MatchMedia, useClass: MockMatchMedia}
76+
]
77+
});
78+
});
79+
80+
describe('with static api', () => {
81+
it('should preserve the static src attribute', () => {
82+
let url = 'https://dummyimage.com/300x300/c72538/ffffff.png';
83+
componentWithTemplate(`
84+
<img src="${url}">
85+
`);
86+
const img = queryFor(fixture, 'img')[0].nativeElement;
87+
88+
fixture.detectChanges();
89+
expect(_.getAttribute( img, 'src')).toEqual(url);
90+
});
91+
92+
it('should work with empty src attributes', () => {
93+
componentWithTemplate(`
94+
<img src="">
95+
`);
96+
const img = queryFor(fixture, 'img')[0].nativeElement;
97+
98+
fixture.detectChanges();
99+
expect(img).toHaveAttributes({
100+
src: ''
101+
});
102+
});
103+
104+
it('should work standard input bindings', () => {
105+
componentWithTemplate(`
106+
<img [src]="defaultSrc" [src.xs]="xsSrc">
107+
`);
108+
const img = queryFor(fixture, 'img')[0].nativeElement;
109+
110+
fixture.detectChanges();
111+
expect(img).toHaveAttributes({
112+
src: 'https://dummyimage.com/300x300/c72538/ffffff.png'
113+
});
114+
115+
let url = 'https://dummyimage.com/700x400/258cc7/fff.png';
116+
fixture.componentInstance.defaultSrc = url;
117+
fixture.detectChanges();
118+
expect(img).toHaveAttributes({ src: url });
119+
120+
});
121+
122+
it('should work when `src` value is not defined', () => {
123+
componentWithTemplate(`
124+
<img src >
125+
`);
126+
127+
const img = queryFor(fixture, 'img')[0].nativeElement;
128+
fixture.detectChanges();
129+
expect(img).toHaveAttributes({
130+
src: ''
131+
});
132+
});
133+
134+
it('should only work with "<img>" elements.', () => {
135+
componentWithTemplate(`
136+
<iframe src.xs="none.png" >
137+
`);
138+
139+
const img = queryFor(fixture, 'iframe')[0].nativeElement;
140+
fixture.detectChanges();
141+
expect(img).not.toHaveAttributes({
142+
src: ''
143+
});
144+
});
145+
146+
});
147+
148+
describe('with responsive api', () => {
149+
150+
it('should work with a isolated image element and responsive srcs', () => {
151+
componentWithTemplate(`
152+
<img [src]="xsSrc"
153+
[src.md]="mdSrc">
154+
`);
155+
fixture.detectChanges();
156+
157+
let img = queryFor(fixture, 'img')[0].nativeElement;
158+
159+
matchMedia.activate('md');
160+
fixture.detectChanges();
161+
expect(img).toBeDefined();
162+
expect(img).toHaveAttributes({
163+
src: SRC_URLS['md'][0]
164+
});
165+
166+
// When activating an unused breakpoint, fallback to default [src] value
167+
matchMedia.activate('xl');
168+
fixture.detectChanges();
169+
expect(img).toHaveAttributes({
170+
src: SRC_URLS['xs'][0]
171+
});
172+
});
173+
174+
it('should work if default [src] is not defined', () => {
175+
componentWithTemplate(`
176+
<img [src.md]="mdSrc">
177+
`);
178+
fixture.detectChanges();
179+
matchMedia.activate('md');
180+
fixture.detectChanges();
181+
182+
let img = queryFor(fixture, 'img')[0].nativeElement;
183+
expect(img).toBeDefined();
184+
expect(img).toHaveAttributes({
185+
src: SRC_URLS['md'][0]
186+
});
187+
188+
// When activating an unused breakpoint, fallback to default [src] value
189+
matchMedia.activate('xl');
190+
fixture.detectChanges();
191+
expect(img).toHaveAttributes({
192+
src: ''
193+
});
194+
});
195+
196+
});
197+
});
198+
199+
// *****************************************************************
200+
// Template Component
201+
// *****************************************************************
202+
203+
@Component({
204+
selector: 'test-src-api',
205+
template: ''
206+
})
207+
export class TestSrcComponent {
208+
defaultSrc = '';
209+
xsSrc = '';
210+
mdSrc = '';
211+
lgSrc = '';
212+
213+
constructor() {
214+
this.defaultSrc = DEFAULT_SRC;
215+
this.xsSrc = SRC_URLS['xs'][0];
216+
this.mdSrc = SRC_URLS['md'][0];
217+
this.lgSrc = SRC_URLS['lg'][0];
218+
219+
}
220+
}
221+
222+

src/lib/api/ext/img-src.ts

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import {
9+
Directive,
10+
ElementRef,
11+
Input,
12+
OnInit,
13+
OnChanges,
14+
Renderer2
15+
} from '@angular/core';
16+
17+
import {BaseFxDirective} from '../core/base';
18+
import {MediaMonitor} from '../../media-query/media-monitor';
19+
20+
/**
21+
* This directive provides a responsive API for the HTML <img> 'src' attribute
22+
* and will update the img.src property upon each responsive activation.
23+
*
24+
* e.g.
25+
* <img src="defaultScene.jpg" src.xs="mobileScene.jpg"></img>
26+
*
27+
* @see https://css-tricks.com/responsive-images-youre-just-changing-resolutions-use-src/
28+
*/
29+
@Directive({
30+
selector: `
31+
img[src.xs], img[src.sm], img[src.md], img[src.lg], img[src.xl],
32+
img[src.lt-sm], img[src.lt-md], img[src.lt-lg], img[src.lt-xl],
33+
img[src.gt-xs], img[src.gt-sm], img[src.gt-md], img[src.gt-lg]
34+
`
35+
})
36+
export class ImgSrcDirective extends BaseFxDirective implements OnInit, OnChanges {
37+
38+
/* tslint:disable */
39+
@Input('src') set srcBase(val) { this.cacheDefaultSrc(val); }
40+
41+
@Input('src.xs') set srcXs(val) { this._cacheInput('srcXs', val); }
42+
@Input('src.sm') set srcSm(val) { this._cacheInput('srcSm', val); }
43+
@Input('src.md') set srcMd(val) { this._cacheInput('srcMd', val); }
44+
@Input('src.lg') set srcLg(val) { this._cacheInput('srcLg', val); }
45+
@Input('src.xl') set srcXl(val) { this._cacheInput('srcXl', val); }
46+
47+
@Input('src.lt-sm') set srcLtSm(val) { this._cacheInput('srcLtSm', val); }
48+
@Input('src.lt-md') set srcLtMd(val) { this._cacheInput('srcLtMd', val); }
49+
@Input('src.lt-lg') set srcLtLg(val) { this._cacheInput('srcLtLg', val); }
50+
@Input('src.lt-xl') set srcLtXl(val) { this._cacheInput('srcLtXl', val); }
51+
52+
@Input('src.gt-xs') set srcGtXs(val) { this._cacheInput('srcGtXs', val); }
53+
@Input('src.gt-sm') set srcGtSm(val) { this._cacheInput('srcGtSm', val); }
54+
@Input('src.gt-md') set srcGtMd(val) { this._cacheInput('srcGtMd', val); }
55+
@Input('src.gt-lg') set srcGtLg(val) { this._cacheInput('srcGtLg', val); }
56+
/* tslint:enable */
57+
58+
constructor(elRef: ElementRef, renderer: Renderer2, monitor: MediaMonitor) {
59+
super(monitor, elRef, renderer);
60+
this._cacheInput('src', elRef.nativeElement.getAttribute('src') || '');
61+
}
62+
63+
/**
64+
* Listen for responsive changes to update the img.src attribute
65+
*/
66+
ngOnInit() {
67+
super.ngOnInit();
68+
69+
if (this.hasResponsiveKeys) {
70+
// Listen for responsive changes
71+
this._listenForMediaQueryChanges('src', this.defaultSrc, () => {
72+
this._updateSrcFor();
73+
});
74+
}
75+
this._updateSrcFor();
76+
}
77+
78+
/**
79+
* Update the 'src' property of the host <img> element
80+
*/
81+
ngOnChanges() {
82+
if (this.hasInitialized) {
83+
this._updateSrcFor();
84+
}
85+
}
86+
87+
/**
88+
* Use the [responsively] activated input value to update
89+
* the host img src attribute or assign a default `img.src=''`
90+
* if the src has not been defined.
91+
*
92+
* Do nothing to standard `<img src="">` usages, only when responsive
93+
* keys are present do we actually call `setAttribute()`
94+
*/
95+
protected _updateSrcFor() {
96+
if (this.hasResponsiveKeys) {
97+
let url = this.activatedValue || this.defaultSrc;
98+
this._renderer.setAttribute(this.nativeElement, 'src', String(url));
99+
}
100+
}
101+
102+
/**
103+
* Cache initial value of 'src', this will be used as fallback when breakpoint
104+
* activations change.
105+
* NOTE: The default 'src' property is not bound using @Input(), so perform
106+
* a post-ngOnInit() lookup of the default src value (if any).
107+
*/
108+
protected cacheDefaultSrc(value?: string) {
109+
this._cacheInput('src', value || '');
110+
}
111+
112+
/**
113+
* Empty values are maintained, undefined values are exposed as ''
114+
*/
115+
protected get defaultSrc(): string {
116+
return this._queryInput('src') || '';
117+
}
118+
119+
/**
120+
* Does the <img> have 1 or more src.<xxx> responsive inputs
121+
* defined... these will be mapped to activated breakpoints.
122+
*/
123+
protected get hasResponsiveKeys() {
124+
return Object.keys(this._inputMap).length > 1;
125+
}
126+
127+
}

src/lib/api/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ export * from './flexbox/flex-order';
2323
export * from './ext/class';
2424
export * from './ext/style';
2525
export * from './ext/show-hide';
26+
export * from './ext/img-src';
2627

src/lib/module.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {LayoutGapDirective} from './api/flexbox/layout-gap';
3131
import {ShowHideDirective} from './api/ext/show-hide';
3232
import {ClassDirective} from './api/ext/class';
3333
import {StyleDirective} from './api/ext/style';
34+
import {ImgSrcDirective} from './api/ext/img-src';
3435

3536
/**
3637
* Since the equivalent results are easily achieved with a css class attached to each
@@ -52,7 +53,8 @@ const ALL_DIRECTIVES = [
5253
FlexAlignDirective,
5354
ShowHideDirective,
5455
ClassDirective,
55-
StyleDirective
56+
StyleDirective,
57+
ImgSrcDirective
5658
];
5759

5860
/**

0 commit comments

Comments
 (0)