Skip to content

Commit

Permalink
feat(srcset): add srcset directive to inject <source> elements to sup…
Browse files Browse the repository at this point in the history
…port 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 angular#81.
  • Loading branch information
benbraou committed Aug 10, 2017
1 parent 695bf37 commit ebc9eb6
Show file tree
Hide file tree
Showing 11 changed files with 497 additions and 9 deletions.
5 changes: 4 additions & 1 deletion src/demo-app/app/docs-layout-responsive/_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Component } from '@angular/core';
<demo-responsive-flex-order class='small-demo'> </demo-responsive-flex-order>
<demo-responsive-show-hide class='small-demo'> </demo-responsive-show-hide>
<demo-responsive-style class='small-demo'> </demo-responsive-style>
<demo-responsive-picture class='small-demo'> </demo-responsive-picture>
`
})
export class DemosResponsiveLayout { }
Expand All @@ -23,6 +24,7 @@ import {DemoResponsiveShowHide} from './responsiveShowHide.demo';
import {DemoResponsiveFlexDirectives} from './responsiveFlexDirective.demo';
import {DemoResponsiveFlexOrder} from './responsiveFlexOrder.demo';
import {DemoResponsiveStyle} from './responsiveStyle.demo';
import {DemoResponsivePicture} from './responsivePicture.demo';

@NgModule({
declarations : [
Expand All @@ -33,7 +35,8 @@ import {DemoResponsiveStyle} from './responsiveStyle.demo';
DemoResponsiveFlexDirectives,
DemoResponsiveFlexOrder,
DemoResponsiveShowHide,
DemoResponsiveStyle
DemoResponsiveStyle,
DemoResponsivePicture
],
imports : [
SharedModule,
Expand Down
53 changes: 53 additions & 0 deletions src/demo-app/app/docs-layout-responsive/responsivePicture.demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {Component} from '@angular/core';

@Component({
moduleId: module.id,
selector: 'demo-responsive-picture',
template: `
<md-card class="card-demo" >
<md-card-title>Responsive Picture</md-card-title>
<md-card-subtitle>
Use the srcset API on an &lt;img&gt; to inject &lt;source&gt; elements within a
&lt;picture&gt; container.
</md-card-subtitle>
<md-card-content>
<div class="containerX">
<div fxLayout="row" fxFlex class="coloredContainerX box">
<picture>
<img style="width:auto;"
src="https://dummyimage.com/400x200/c7c224/000.png&text=default"
srcset.md="https://dummyimage.com/500x200/76c720/fff.png&text=md"
srcset.sm="https://dummyimage.com/400x200/b925c7/fff.png&text=sm"
srcset.lt-sm="https://dummyimage.com/300x200/c7751e/fff.png&text=lt-sm(1x) 1x,
https://dummyimage.com/300x200/f0b16e/fff.png&text=lt-sm(2x) 2x,
https://dummyimage.com/300x200/f6ca9a/fff.png&text=lt-sm(3x) 3x"
srcset.gt-lg="https://dummyimage.com/700x200/258cc7/fff.png&text=gt-lg" >
</picture>
</div>
</div>
</md-card-content>
<md-card-content>
<pre>
&lt;picture&gt;
&lt;img style="width:auto;"
src="https://dummyimage.com/400x200/c7c224/000.png&text=default"
srcset.md="https://dummyimage.com/500x200/76c720/fff.png&text=md"
srcset.sm="https://dummyimage.com/400x200/b925c7/fff.png&text=sm"
srcset.lt-sm="https://dummyimage.com/300x200/c7751e/fff.png&text=lt-sm(1x) 1x,
https://dummyimage.com/300x200/f0b16e/fff.png&text=lt-sm(2x) 2x,
https://dummyimage.com/300x200/f6ca9a/fff.png&text=lt-sm(3x) 3x"
srcset.gt-lg="https://dummyimage.com/700x200/258cc7/fff.png&text=gt-lg" &gt;
&lt;/picture&gt;
</pre>
</md-card-content>
<md-card-footer style="width:95%">
<media-query-status></media-query-status>
</md-card-footer>
</md-card>
`
})
export class DemoResponsivePicture {
}
10 changes: 7 additions & 3 deletions src/lib/flexbox/api/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges {
return this._elementRef.nativeElement.parentNode;
}

protected get nativeElement(): any {
return this._elementRef.nativeElement;
}

/**
* Access the current value (if any) of the @Input property.
*/
Expand Down Expand Up @@ -123,7 +127,7 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges {
* and optional restore it when the mediaQueries deactivate
*/
protected _getDisplayStyle(source?: HTMLElement): string {
let element: HTMLElement = source || this._elementRef.nativeElement;
let element: HTMLElement = source || this.nativeElement;
let value = this._lookupStyle(element, 'display');

// Note: 'inline' is the default of all elements, unless UA stylesheet overrides
Expand Down Expand Up @@ -187,7 +191,7 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges {
value?: string | number,
nativeElement?: any) {
let styles = {};
let element = nativeElement || this._elementRef.nativeElement;
let element = nativeElement || this.nativeElement;

if (typeof style === 'string') {
styles[style] = value;
Expand Down Expand Up @@ -247,7 +251,7 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges {
* Special accessor to query for all child 'element' nodes regardless of type, class, etc.
*/
protected get childrenNodes() {
const obj = this._elementRef.nativeElement.children;
const obj = this.nativeElement.children;
const buffer = [];

// iterate backwards ensuring that length is an UInt32
Expand Down
232 changes: 232 additions & 0 deletions src/lib/flexbox/api/img-srcset.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
/**
* @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, OnInit} 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 SRCSET_URLS_MAP = {
'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('srcset directive', () => {
let fixture: ComponentFixture<any>;
let matchMedia: MockMatchMedia;
let breakpoints: BreakPointRegistry;

let componentWithTemplate = (template: string) => {
fixture = makeCreateTestComponent(() => TestSrcsetComponent)(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: [TestSrcsetComponent],
providers: [
BreakPointRegistry, DEFAULT_BREAKPOINTS_PROVIDER,
{provide: MatchMedia, useClass: MockMatchMedia}
]
});
});

it('should work when no srcset flex-layout directive is used', () => {
const template = `
<picture>
<img style="width:auto;" src="${DEFAULT_SRC}" >
</picture>
`;
componentWithTemplate(template);
fixture.detectChanges();

const nodes = queryFor(fixture, 'source');
const pictureElt = queryFor(fixture, 'picture')[0].nativeElement;

expect(nodes.length).toBe(0);
expect(pictureElt.children.length).toEqual(1);
expect(_.tagName(_.lastElementChild(pictureElt))).toEqual('IMG');
});

it('should keep img as the last child tag of <picture> after source tags injection', () => {
const template = `
<div>
<picture>
<img style="width:auto;"
src="${DEFAULT_SRC}"
srcset.gt-xs="${SRCSET_URLS_MAP['gt-xs'][0]}"
srcset.lt-lg="${SRCSET_URLS_MAP['lt-lg'][0]}" >
</picture>
</div>
`;
componentWithTemplate(template);
fixture.detectChanges();

const pictureElt = queryFor(fixture, 'picture')[0].nativeElement;

expect(_.tagName(_.lastElementChild(pictureElt))).toEqual('IMG');
});

it('should inject source elements from largest to smallest corresponding media queries', () => {
const template = `
<picture>
<img style="width:auto;"
src="${DEFAULT_SRC}"
srcset.xs="${SRCSET_URLS_MAP['xs'][0]}"
srcset.lg="${SRCSET_URLS_MAP['lg'][0]}"
srcset.md="${SRCSET_URLS_MAP['md'][0]}" >
</picture>
`;
componentWithTemplate(template);
fixture.detectChanges();

const nodes = queryFor(fixture, 'source');

expect(nodes.length).toBe(3);
expect(nodes[0].nativeElement).toHaveAttributes({
srcset: `${SRCSET_URLS_MAP['lg'][0]}`,
media: breakpoints.findByAlias('lg').mediaQuery
});
expect(nodes[1].nativeElement).toHaveAttributes({
srcset: `${SRCSET_URLS_MAP['md'][0]}`,
media: breakpoints.findByAlias('md').mediaQuery
});
expect(nodes[2].nativeElement).toHaveAttributes({
srcset: `${SRCSET_URLS_MAP['xs'][0]}`,
media: breakpoints.findByAlias('xs').mediaQuery
});
});

it('should update source elements srcset values when srcset input properties change', () => {
const template = `
<picture>
<img style="width:auto;"
src="${DEFAULT_SRC}"
[srcset.xs]="xsSrcSet"
[srcset.lg]="lgSrcSet"
[srcset.md]="mdSrcSet" >
</picture>
`;
componentWithTemplate(template);
fixture.detectChanges();

fixture.componentInstance.xsSrcSet = SRCSET_URLS_MAP['xs'][1];
fixture.componentInstance.mdSrcSet = SRCSET_URLS_MAP['md'][1];
fixture.componentInstance.lgSrcSet = SRCSET_URLS_MAP['lg'][1];
fixture.detectChanges();

let nodes = queryFor(fixture, 'source');

expect(nodes.length).toBe(3);
expect(nodes[0].nativeElement).toHaveAttributes({
srcset: `${SRCSET_URLS_MAP['lg'][1]}`,
media: breakpoints.findByAlias('lg').mediaQuery
});
expect(nodes[1].nativeElement).toHaveAttributes({
srcset: `${SRCSET_URLS_MAP['md'][1]}`,
media: breakpoints.findByAlias('md').mediaQuery
});
expect(nodes[2].nativeElement).toHaveAttributes({
srcset: `${SRCSET_URLS_MAP['xs'][1]}`,
media: breakpoints.findByAlias('xs').mediaQuery
});
});

it('should work with overlapping breakpoints', () => {
const template = `
<picture>
<img style="width:auto;"
src="${DEFAULT_SRC}"
srcset.lt-xl="${SRCSET_URLS_MAP['lt-xl'][0]}"
srcset.xs="${SRCSET_URLS_MAP['xs'][0]}"
srcset.lt-lg="${SRCSET_URLS_MAP['lt-lg'][0]}" >
</picture>
`;
componentWithTemplate(template);
fixture.detectChanges();

let nodes = queryFor(fixture, 'source');
expect(nodes[0].nativeElement).toHaveAttributes({
srcset: `${SRCSET_URLS_MAP['lt-xl'][0]}`,
media: breakpoints.findByAlias('lt-xl').mediaQuery
});
expect(nodes[1].nativeElement).toHaveAttributes({
srcset: `${SRCSET_URLS_MAP['lt-lg'][0]}`,
media: breakpoints.findByAlias('lt-lg').mediaQuery
});
expect(nodes[2].nativeElement).toHaveAttributes({
srcset: `${SRCSET_URLS_MAP['xs'][0]}`,
media: breakpoints.findByAlias('xs').mediaQuery
});
});
});

// *****************************************************************
// Template Component
// *****************************************************************

@Component({
selector: 'test-srcset-api',
template: ''
})
export class TestSrcsetComponent implements OnInit {
xsSrcSet: string;
mdSrcSet: string;
lgSrcSet: string;
constructor() {
this.xsSrcSet = SRCSET_URLS_MAP['xs'][0];
this.mdSrcSet = SRCSET_URLS_MAP['md'][0];
this.lgSrcSet = SRCSET_URLS_MAP['lg'][0];
}

ngOnInit() {
}
}


Loading

0 comments on commit ebc9eb6

Please sign in to comment.