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

Commit da5265b

Browse files
feat(api): add responsive API for img elements
* 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 * fix ImgSrcsetDirective to support usages without `<picture>` parents Closes #366, Fixes #81, Fixes #376.
1 parent ff8f620 commit da5265b

35 files changed

+530
-122
lines changed

src/lib/flexbox/api/base-adapter.ts renamed to src/lib/api/core/base-adapter.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import {ElementRef, Renderer2} from '@angular/core';
99

1010
import {BaseFxDirective} from './base';
11-
import {ResponsiveActivation} from './../responsive/responsive-activation';
11+
import {ResponsiveActivation} from './responsive-activation';
1212
import {MediaQuerySubscriber} from '../../media-query/media-change';
1313
import {MediaMonitor} from '../../media-query/media-monitor';
1414

src/lib/flexbox/api/base.ts renamed to src/lib/api/core/base.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
applyStyleToElements
2020
} from '../../utils/style-utils';
2121

22-
import {ResponsiveActivation, KeyOptions} from '../responsive/responsive-activation';
22+
import {ResponsiveActivation, KeyOptions} from '../core/responsive-activation';
2323
import {MediaMonitor} from '../../media-query/media-monitor';
2424
import {MediaQuerySubscriber} from '../../media-query/media-change';
2525

src/lib/flexbox/responsive/responsive-activation.ts renamed to src/lib/api/core/responsive-activation.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export class ResponsiveActivation {
6161
* important when several media queries are 'registered' and from which, the browser uses the
6262
* first matching media query.
6363
*/
64-
get registryFromLargest():BreakPointX[] {
64+
get registryFromLargest(): BreakPointX[] {
6565
return [...this._registryMap].reverse();
6666
}
6767

File renamed without changes.

src/lib/flexbox/api/class.ts renamed to src/lib/api/ext/class.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ import {
2222
} from '@angular/core';
2323
import {NgClass} from '@angular/common';
2424

25-
import {BaseFxDirective} from './base';
26-
import {BaseFxDirectiveAdapter} from './base-adapter';
25+
import {BaseFxDirective} from '../core/base';
26+
import {BaseFxDirectiveAdapter} from '../core/base-adapter';
2727
import {MediaChange} from '../../media-query/media-change';
2828
import {MediaMonitor} from '../../media-query/media-monitor';
2929

File renamed without changes.

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

+191
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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+
22+
const SRC_URLS = {
23+
'xs': [
24+
'https://dummyimage.com/300x200/c7751e/fff.png',
25+
'https://dummyimage.com/300x200/c7751e/000.png'
26+
],
27+
'gt-xs': [
28+
'https://dummyimage.com/400x250/c7c224/fff.png',
29+
'https://dummyimage.com/400x250/c7c224/000.png'
30+
],
31+
'md': [
32+
'https://dummyimage.com/500x300/76c720/fff.png',
33+
'https://dummyimage.com/500x300/76c720/000.png'
34+
],
35+
'lt-lg': [
36+
'https://dummyimage.com/600x350/25c794/fff.png',
37+
'https://dummyimage.com/600x350/25c794/000.png'
38+
],
39+
'lg': [
40+
'https://dummyimage.com/700x400/258cc7/fff.png',
41+
'https://dummyimage.com/700x400/258cc7/000.png'
42+
],
43+
'lt-xl': [
44+
'https://dummyimage.com/800x500/b925c7/ffffff.png',
45+
'https://dummyimage.com/800x500/b925c7/000.png'
46+
]
47+
};
48+
const DEFAULT_SRC = 'https://dummyimage.com/300x300/c72538/ffffff.png';
49+
50+
describe('img-src directive', () => {
51+
let fixture: ComponentFixture<any>;
52+
let matchMedia: MockMatchMedia;
53+
let breakpoints: BreakPointRegistry;
54+
55+
let componentWithTemplate = (template: string) => {
56+
fixture = makeCreateTestComponent(() => TestSrcsetComponent)(template);
57+
58+
inject([MatchMedia, BreakPointRegistry],
59+
(_matchMedia: MockMatchMedia, _breakpoints: BreakPointRegistry) => {
60+
matchMedia = _matchMedia;
61+
breakpoints = _breakpoints;
62+
})();
63+
};
64+
65+
beforeEach(() => {
66+
jasmine.addMatchers(customMatchers);
67+
68+
// Configure testbed to prepare services
69+
TestBed.configureTestingModule({
70+
imports: [CommonModule, FlexLayoutModule],
71+
declarations: [TestSrcsetComponent],
72+
providers: [
73+
BreakPointRegistry, DEFAULT_BREAKPOINTS_PROVIDER,
74+
{provide: MatchMedia, useClass: MockMatchMedia}
75+
]
76+
});
77+
});
78+
79+
describe('with static api', () => {
80+
it('should preserve the static src attribute', () => {
81+
componentWithTemplate(`
82+
<img src="https://dummyimage.com/300x300/c72538/ffffff.png">
83+
`);
84+
const img = queryFor(fixture, 'img')[0].nativeElement;
85+
86+
fixture.detectChanges();
87+
expect(img).toHaveAttributes({
88+
src: 'https://dummyimage.com/300x300/c72538/ffffff.png'
89+
});
90+
});
91+
92+
it('should work standard input bindings', () => {
93+
componentWithTemplate(`
94+
<img [src]="defaultSrc" [src.xs]="xsSrc">
95+
`);
96+
const img = queryFor(fixture, 'img')[0].nativeElement;
97+
98+
fixture.detectChanges();
99+
expect(img).toHaveAttributes({
100+
src: 'https://dummyimage.com/300x300/c72538/ffffff.png'
101+
});
102+
});
103+
104+
it('should work when no `src` value is defined', () => {
105+
componentWithTemplate(`
106+
<img src="" >
107+
`);
108+
109+
const img = queryFor(fixture, 'img')[0].nativeElement;
110+
fixture.detectChanges();
111+
expect(img).toHaveAttributes({
112+
src: ''
113+
});
114+
});
115+
});
116+
117+
describe('with responsive api', () => {
118+
119+
it('should work with a isolated image element and responsive srcs', () => {
120+
componentWithTemplate(`
121+
<img [src]="xsSrc"
122+
[src.md]="mdSrc">
123+
`);
124+
fixture.detectChanges();
125+
126+
let img = queryFor(fixture, 'img')[0].nativeElement;
127+
128+
matchMedia.activate('md');
129+
fixture.detectChanges();
130+
expect(img).toBeDefined();
131+
expect(img).toHaveAttributes({
132+
src: SRC_URLS['md'][0]
133+
});
134+
135+
// When activating an unused breakpoint, fallback to default [src] value
136+
matchMedia.activate('xl');
137+
fixture.detectChanges();
138+
expect(img).toHaveAttributes({
139+
src: SRC_URLS['xs'][0]
140+
});
141+
});
142+
143+
it('should work use [src] if default [src] is not defined', () => {
144+
componentWithTemplate(`
145+
<img [src.md]="mdSrc">
146+
`);
147+
fixture.detectChanges();
148+
matchMedia.activate('md');
149+
fixture.detectChanges();
150+
151+
let img = queryFor(fixture, 'img')[0].nativeElement;
152+
expect(img).toBeDefined();
153+
expect(img).toHaveAttributes({
154+
src: SRC_URLS['md'][0]
155+
});
156+
157+
// When activating an unused breakpoint, fallback to default [src] value
158+
matchMedia.activate('xl');
159+
fixture.detectChanges();
160+
expect(img).toHaveAttributes({
161+
src: ''
162+
});
163+
});
164+
165+
});
166+
});
167+
168+
// *****************************************************************
169+
// Template Component
170+
// *****************************************************************
171+
172+
@Component({
173+
selector: 'test-src-api',
174+
template: ''
175+
})
176+
export class TestSrcsetComponent {
177+
defaultSrc = '';
178+
xsSrc = '';
179+
mdSrc = '';
180+
lgSrc = '';
181+
182+
constructor() {
183+
this.defaultSrc = DEFAULT_SRC;
184+
this.xsSrc = SRC_URLS['xs'][0];
185+
this.mdSrc = SRC_URLS['md'][0];
186+
this.lgSrc = SRC_URLS['lg'][0];
187+
188+
}
189+
}
190+
191+

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

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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+
import {ɵgetDOM as getDom} from '@angular/platform-browser';
17+
18+
import {BaseFxDirective} from '../core/base';
19+
import {MediaMonitor} from '../../media-query/media-monitor';
20+
21+
/**
22+
* This directive provides a responsive API for the HTML <img> 'src' attribute
23+
* and will update the img.src property upon each responsive activation.
24+
* Note: This solution is complementary to using the `img.srcset` approaches. Both are
25+
* published to support developer preference.
26+
*
27+
* e.g.
28+
* <img src="defaultScene.jpg" src.xs="mobileScene.jpg"></img>
29+
*
30+
* @see https://css-tricks.com/responsive-images-youre-just-changing-resolutions-use-src/
31+
*/
32+
@Directive({
33+
selector: `
34+
[src],
35+
[src.xs], [src.sm], [src.md], [src.lg], [src.xl],
36+
[src.lt-sm], [src.lt-md], [src.lt-lg], [src.lt-xl],
37+
[src.gt-xs], [src.gt-sm], [src.gt-md], [src.gt-lg]
38+
`
39+
})
40+
export class ImgSrcDirective extends BaseFxDirective implements OnInit, OnChanges {
41+
42+
/* tslint:disable */
43+
@Input('src') set srcBase(val) { this.cacheDefaultSrc(val); }
44+
45+
@Input('src.xs') set srcXs(val) { this._cacheInput('srcXs', val); }
46+
@Input('src.sm') set srcSm(val) { this._cacheInput('srcSm', val); }
47+
@Input('src.md') set srcMd(val) { this._cacheInput('srcMd', val); }
48+
@Input('src.lg') set srcLg(val) { this._cacheInput('srcLg', val); }
49+
@Input('src.xl') set srcXl(val) { this._cacheInput('srcXl', val); }
50+
51+
@Input('src.lt-sm') set srcLtSm(val) { this._cacheInput('srcLtSm', val); }
52+
@Input('src.lt-md') set srcLtMd(val) { this._cacheInput('srcLtMd', val); }
53+
@Input('src.lt-lg') set srcLtLg(val) { this._cacheInput('srcLtLg', val); }
54+
@Input('src.lt-xl') set srcLtXl(val) { this._cacheInput('srcLtXl', val); }
55+
56+
@Input('src.gt-xs') set srcGtXs(val) { this._cacheInput('srcGtXs', val); }
57+
@Input('src.gt-sm') set srcGtSm(val) { this._cacheInput('srcGtSm', val); }
58+
@Input('src.gt-md') set srcGtMd(val) { this._cacheInput('srcGtMd', val); }
59+
@Input('src.gt-lg') set srcGtLg(val) { this._cacheInput('srcGtLg', val); }
60+
/* tslint:enable */
61+
62+
constructor(elRef: ElementRef, renderer: Renderer2, monitor: MediaMonitor) {
63+
super(monitor, elRef, renderer);
64+
}
65+
66+
/**
67+
* Listen for responsive changes to update the img.src attribute
68+
*/
69+
ngOnInit() {
70+
super.ngOnInit();
71+
72+
// Cache initial value of `src` to use as responsive fallback
73+
this.cacheDefaultSrc(this.defaultSrc);
74+
75+
// Listen for responsive changes
76+
this._listenForMediaQueryChanges('src', this.defaultSrc, () => {
77+
this._updateSrcFor();
78+
});
79+
this._updateSrcFor();
80+
}
81+
82+
/**
83+
* Update the 'src' property of the host <img> element
84+
*/
85+
ngOnChanges() {
86+
if (this.hasInitialized) {
87+
this._updateSrcFor();
88+
}
89+
}
90+
91+
/**
92+
* Use the [responsively] activated input value to update
93+
* the host img src attribute.
94+
*/
95+
protected _updateSrcFor() {
96+
if (this._mqActivation) {
97+
let url = this._mqActivation.activatedInput || '';
98+
this._renderer.setAttribute(this.nativeElement, 'src', url);
99+
}
100+
}
101+
102+
103+
/**
104+
* Cache initial value of 'src', this will be used as fallback when breakpoint
105+
* activations change.
106+
* NOTE: The default 'src' property is not bound using @Input(), so perform
107+
* a post-ngOnInit() lookup of the default src value (if any).
108+
*/
109+
protected cacheDefaultSrc(value?: string) {
110+
const currentVal = this._queryInput('src');
111+
if (typeof currentVal == 'undefined') {
112+
this._cacheInput('src', value || '');
113+
}
114+
}
115+
116+
/**
117+
* Empty values are maintained, undefined values are exposed as ''
118+
*/
119+
protected get defaultSrc(): string {
120+
let attrVal = getDom().getAttribute(this.nativeElement, 'src');
121+
return this._queryInput('src') || attrVal || '';
122+
}
123+
}

0 commit comments

Comments
 (0)