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

Commit d65e00a

Browse files
committed
feat(srcset): add srcset directive 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> element. Closes #81.
1 parent 0f13b14 commit d65e00a

File tree

12 files changed

+484
-13
lines changed

12 files changed

+484
-13
lines changed

src/demo-app/app/docs-layout-responsive/_module.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Component } from '@angular/core';
99
<demo-responsive-flex-order class='small-demo'> </demo-responsive-flex-order>
1010
<demo-responsive-show-hide class='small-demo'> </demo-responsive-show-hide>
1111
<demo-responsive-style class='small-demo'> </demo-responsive-style>
12+
<demo-responsive-picture class='small-demo'> </demo-responsive-picture>
1213
`
1314
})
1415
export class DemosResponsiveLayout { }
@@ -23,6 +24,7 @@ import {DemoResponsiveShowHide} from './responsiveShowHide.demo';
2324
import {DemoResponsiveFlexDirectives} from './responsiveFlexDirective.demo';
2425
import {DemoResponsiveFlexOrder} from './responsiveFlexOrder.demo';
2526
import {DemoResponsiveStyle} from './responsiveStyle.demo';
27+
import {DemoResponsivePicture} from './responsivePicture.demo';
2628

2729
@NgModule({
2830
declarations : [
@@ -33,7 +35,8 @@ import {DemoResponsiveStyle} from './responsiveStyle.demo';
3335
DemoResponsiveFlexDirectives,
3436
DemoResponsiveFlexOrder,
3537
DemoResponsiveShowHide,
36-
DemoResponsiveStyle
38+
DemoResponsiveStyle,
39+
DemoResponsivePicture
3740
],
3841
imports : [
3942
SharedModule,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {Component} from '@angular/core';
2+
3+
@Component({
4+
moduleId: module.id,
5+
selector: 'demo-responsive-picture',
6+
template: `
7+
8+
<md-card class="card-demo" >
9+
<md-card-title>Responsive Picture</md-card-title>
10+
<md-card-subtitle>
11+
Use the srcset API on an &lt;img&gt; to inject &lt;source&gt; elements within a
12+
&lt;picture&gt; container.
13+
</md-card-subtitle>
14+
15+
<md-card-content>
16+
<div class="containerX">
17+
<div fxLayout="row" fxFlex class="coloredContainerX box">
18+
<picture>
19+
<img style="width:auto;"
20+
src="https://dummyimage.com/400x200/c7c224/000.png&text=default"
21+
srcset.md="https://dummyimage.com/500x200/76c720/fff.png&text=md"
22+
srcset.sm="https://dummyimage.com/400x200/b925c7/fff.png&text=sm"
23+
srcset.lt-sm="https://dummyimage.com/300x200/c7751e/fff.png&text=lt-sm"
24+
srcset.gt-md="https://dummyimage.com/700x200/258cc7/fff.png&text=gt-md" >
25+
</picture>
26+
</div>
27+
</div>
28+
</md-card-content>
29+
<md-card-content>
30+
<pre>
31+
&lt;picture&gt;
32+
&lt;img style="width:auto;"
33+
src="https://dummyimage.com/400x200/c7c224/000.png&text=default"
34+
srcset.md="https://dummyimage.com/500x200/76c720/fff.png&text=md"
35+
srcset.sm="https://dummyimage.com/400x200/b925c7/fff.png&text=sm"
36+
srcset.lt-sm="https://dummyimage.com/300x200/c7751e/fff.png&text=lt-sm"
37+
srcset.gt-md="https://dummyimage.com/700x200/258cc7/fff.png&text=gt-md" &gt;
38+
&lt;/picture&gt;
39+
</pre>
40+
</md-card-content>
41+
42+
<md-card-footer style="width:95%">
43+
<media-query-status></media-query-status>
44+
</md-card-footer>
45+
</md-card>
46+
`
47+
})
48+
export class DemoResponsivePicture {
49+
}

src/lib/flexbox/api/base.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges {
117117
* and optional restore it when the mediaQueries deactivate
118118
*/
119119
protected _getDisplayStyle(source?: HTMLElement): string {
120-
let element: HTMLElement = source || this._elementRef.nativeElement;
120+
let element: HTMLElement = source || this.nativeElement;
121121
let value = this._lookupStyle(element, 'display');
122122

123123
return value ? value.trim() : ((element.nodeType === 1) ? 'block' : 'inline-block');
@@ -174,7 +174,7 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges {
174174
value?: string | number,
175175
nativeElement?: any) {
176176
let styles = {};
177-
let element = nativeElement || this._elementRef.nativeElement;
177+
let element = nativeElement || this.nativeElement;
178178

179179
if (typeof style === 'string') {
180180
styles[style] = value;
@@ -234,7 +234,7 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges {
234234
* Special accessor to query for all child 'element' nodes regardless of type, class, etc.
235235
*/
236236
protected get childrenNodes() {
237-
const obj = this._elementRef.nativeElement.children;
237+
const obj = this.nativeElement.children;
238238
const buffer = [];
239239

240240
// iterate backwards ensuring that length is an UInt32
@@ -244,6 +244,14 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges {
244244
return buffer;
245245
}
246246

247+
protected get nativeElement(): any {
248+
return this._elementRef.nativeElement;
249+
}
250+
251+
protected get parentElement(): any {
252+
return this.nativeElement.parentNode;
253+
}
254+
247255
/**
248256
* Fast validator for presence of attribute on the host element
249257
*/

src/lib/flexbox/api/flex.ts

-4
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,4 @@ export class FlexDirective extends BaseFxDirective implements OnInit, OnChanges,
253253

254254
return extendObject(css, {'box-sizing': 'border-box'});
255255
}
256-
257-
protected get parentElement(): any {
258-
return this._elementRef.nativeElement.parentNode;
259-
}
260256
}
+232
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
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, OnInit} 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 SRCSET_URLS_MAP = {
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('srcset directive', () => {
52+
let fixture: ComponentFixture<any>;
53+
let matchMedia: MockMatchMedia;
54+
let breakpoints: BreakPointRegistry;
55+
56+
let componentWithTemplate = (template: string) => {
57+
fixture = makeCreateTestComponent(() => TestSrcsetComponent)(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: [TestSrcsetComponent],
73+
providers: [
74+
BreakPointRegistry, DEFAULT_BREAKPOINTS_PROVIDER,
75+
{provide: MatchMedia, useClass: MockMatchMedia}
76+
]
77+
});
78+
});
79+
80+
it('should work when no srcset flex-layout directive is used', () => {
81+
const template = `
82+
<picture>
83+
<img style="width:auto;" src="${DEFAULT_SRC}" >
84+
</picture>
85+
`;
86+
componentWithTemplate(template);
87+
fixture.detectChanges();
88+
89+
const nodes = queryFor(fixture, 'source');
90+
const pictureElt = queryFor(fixture, 'picture')[0].nativeElement;
91+
92+
expect(nodes.length).toBe(0);
93+
expect(pictureElt.children.length).toEqual(1);
94+
expect(_.tagName(_.lastElementChild(pictureElt))).toEqual('IMG');
95+
});
96+
97+
it('should keep img as the last child tag of <picture> after source tags injection', () => {
98+
const template = `
99+
<div>
100+
<picture>
101+
<img style="width:auto;"
102+
src="${DEFAULT_SRC}"
103+
srcset.gt-xs="${SRCSET_URLS_MAP['gt-xs'][0]}"
104+
srcset.lt-lg="${SRCSET_URLS_MAP['lt-lg'][0]}" >
105+
</picture>
106+
</div>
107+
`;
108+
componentWithTemplate(template);
109+
fixture.detectChanges();
110+
111+
const pictureElt = queryFor(fixture, 'picture')[0].nativeElement;
112+
113+
expect(_.tagName(_.lastElementChild(pictureElt))).toEqual('IMG');
114+
});
115+
116+
it('should inject source elements from largest to smallest corresponding media queries', () => {
117+
const template = `
118+
<picture>
119+
<img style="width:auto;"
120+
src="${DEFAULT_SRC}"
121+
srcset.xs="${SRCSET_URLS_MAP['xs'][0]}"
122+
srcset.lg="${SRCSET_URLS_MAP['lg'][0]}"
123+
srcset.md="${SRCSET_URLS_MAP['md'][0]}" >
124+
</picture>
125+
`;
126+
componentWithTemplate(template);
127+
fixture.detectChanges();
128+
129+
const nodes = queryFor(fixture, 'source');
130+
131+
expect(nodes.length).toBe(3);
132+
expect(nodes[0].nativeElement).toHaveAttributes({
133+
srcset: `${SRCSET_URLS_MAP['lg'][0]}`,
134+
media: breakpoints.findByAlias('lg').mediaQuery
135+
});
136+
expect(nodes[1].nativeElement).toHaveAttributes({
137+
srcset: `${SRCSET_URLS_MAP['md'][0]}`,
138+
media: breakpoints.findByAlias('md').mediaQuery
139+
});
140+
expect(nodes[2].nativeElement).toHaveAttributes({
141+
srcset: `${SRCSET_URLS_MAP['xs'][0]}`,
142+
media: breakpoints.findByAlias('xs').mediaQuery
143+
});
144+
});
145+
146+
it('should update source elements srcset values when srcset input properties change', () => {
147+
const template = `
148+
<picture>
149+
<img style="width:auto;"
150+
src="${DEFAULT_SRC}"
151+
[srcset.xs]="xsSrcSet"
152+
[srcset.lg]="lgSrcSet"
153+
[srcset.md]="mdSrcSet" >
154+
</picture>
155+
`;
156+
componentWithTemplate(template);
157+
fixture.detectChanges();
158+
159+
fixture.componentInstance.xsSrcSet = SRCSET_URLS_MAP['xs'][1];
160+
fixture.componentInstance.mdSrcSet = SRCSET_URLS_MAP['md'][1];
161+
fixture.componentInstance.lgSrcSet = SRCSET_URLS_MAP['lg'][1];
162+
fixture.detectChanges();
163+
164+
let nodes = queryFor(fixture, 'source');
165+
166+
expect(nodes.length).toBe(3);
167+
expect(nodes[0].nativeElement).toHaveAttributes({
168+
srcset: `${SRCSET_URLS_MAP['lg'][1]}`,
169+
media: breakpoints.findByAlias('lg').mediaQuery
170+
});
171+
expect(nodes[1].nativeElement).toHaveAttributes({
172+
srcset: `${SRCSET_URLS_MAP['md'][1]}`,
173+
media: breakpoints.findByAlias('md').mediaQuery
174+
});
175+
expect(nodes[2].nativeElement).toHaveAttributes({
176+
srcset: `${SRCSET_URLS_MAP['xs'][1]}`,
177+
media: breakpoints.findByAlias('xs').mediaQuery
178+
});
179+
});
180+
181+
it('should work with overlapping breakpoints', () => {
182+
const template = `
183+
<picture>
184+
<img style="width:auto;"
185+
src="${DEFAULT_SRC}"
186+
srcset.lt-xl="${SRCSET_URLS_MAP['lt-xl'][0]}"
187+
srcset.xs="${SRCSET_URLS_MAP['xs'][0]}"
188+
srcset.lt-lg="${SRCSET_URLS_MAP['lt-lg'][0]}" >
189+
</picture>
190+
`;
191+
componentWithTemplate(template);
192+
fixture.detectChanges();
193+
194+
let nodes = queryFor(fixture, 'source');
195+
expect(nodes[0].nativeElement).toHaveAttributes({
196+
srcset: `${SRCSET_URLS_MAP['lt-xl'][0]}`,
197+
media: breakpoints.findByAlias('lt-xl').mediaQuery
198+
});
199+
expect(nodes[1].nativeElement).toHaveAttributes({
200+
srcset: `${SRCSET_URLS_MAP['lt-lg'][0]}`,
201+
media: breakpoints.findByAlias('lt-lg').mediaQuery
202+
});
203+
expect(nodes[2].nativeElement).toHaveAttributes({
204+
srcset: `${SRCSET_URLS_MAP['xs'][0]}`,
205+
media: breakpoints.findByAlias('xs').mediaQuery
206+
});
207+
});
208+
});
209+
210+
// *****************************************************************
211+
// Template Component
212+
// *****************************************************************
213+
214+
@Component({
215+
selector: 'test-srcset-api',
216+
template: ''
217+
})
218+
export class TestSrcsetComponent implements OnInit {
219+
xsSrcSet: string;
220+
mdSrcSet: string;
221+
lgSrcSet: string;
222+
constructor() {
223+
this.xsSrcSet = SRCSET_URLS_MAP['xs'][0];
224+
this.mdSrcSet = SRCSET_URLS_MAP['md'][0];
225+
this.lgSrcSet = SRCSET_URLS_MAP['lg'][0];
226+
}
227+
228+
ngOnInit() {
229+
}
230+
}
231+
232+

0 commit comments

Comments
 (0)