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

Commit

Permalink
feat(api, img-src): add ImgSrcDirective for responsive API for img el…
Browse files Browse the repository at this point in the history
…ements

* 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.
  • Loading branch information
ThomasBurleson committed Aug 17, 2017
1 parent 8f93600 commit 15bdecb
Show file tree
Hide file tree
Showing 35 changed files with 530 additions and 122 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import {ElementRef, Renderer2} from '@angular/core';

import {BaseFxDirective} from './base';
import {ResponsiveActivation} from './../responsive/responsive-activation';
import {ResponsiveActivation} from './responsive-activation';
import {MediaQuerySubscriber} from '../../media-query/media-change';
import {MediaMonitor} from '../../media-query/media-monitor';

Expand Down
2 changes: 1 addition & 1 deletion src/lib/flexbox/api/base.ts → src/lib/api/core/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
applyStyleToElements
} from '../../utils/style-utils';

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class ResponsiveActivation {
* important when several media queries are 'registered' and from which, the browser uses the
* first matching media query.
*/
get registryFromLargest():BreakPointX[] {
get registryFromLargest(): BreakPointX[] {
return [...this._registryMap].reverse();
}

Expand Down
File renamed without changes.
4 changes: 2 additions & 2 deletions src/lib/flexbox/api/class.ts → src/lib/api/ext/class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import {
} from '@angular/core';
import {NgClass} from '@angular/common';

import {BaseFxDirective} from './base';
import {BaseFxDirectiveAdapter} from './base-adapter';
import {BaseFxDirective} from '../core/base';
import {BaseFxDirectiveAdapter} from '../core/base-adapter';
import {MediaChange} from '../../media-query/media-change';
import {MediaMonitor} from '../../media-query/media-monitor';

Expand Down
File renamed without changes.
191 changes: 191 additions & 0 deletions src/lib/api/ext/img-src.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/**
* @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';

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(() => 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}
]
});
});

describe('with static api', () => {
it('should preserve the static src attribute', () => {
componentWithTemplate(`
<img src="https://dummyimage.com/300x300/c72538/ffffff.png">
`);
const img = queryFor(fixture, 'img')[0].nativeElement;

fixture.detectChanges();
expect(img).toHaveAttributes({
src: 'https://dummyimage.com/300x300/c72538/ffffff.png'
});
});

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'
});
});

it('should work when no `src` value is defined', () => {
componentWithTemplate(`
<img src="" >
`);

const img = queryFor(fixture, 'img')[0].nativeElement;
fixture.detectChanges();
expect(img).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 use [src] 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 TestSrcsetComponent {
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];

}
}


123 changes: 123 additions & 0 deletions src/lib/api/ext/img-src.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
* @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 {ɵgetDOM as getDom} from '@angular/platform-browser';

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.
* Note: This solution is complementary to using the `img.srcset` approaches. Both are
* published to support developer preference.
*
* 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: `
[src],
[src.xs], [src.sm], [src.md], [src.lg], [src.xl],
[src.lt-sm], [src.lt-md], [src.lt-lg], [src.lt-xl],
[src.gt-xs], [src.gt-sm], [src.gt-md], [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);
}

/**
* Listen for responsive changes to update the img.src attribute
*/
ngOnInit() {
super.ngOnInit();

// Cache initial value of `src` to use as responsive fallback
this.cacheDefaultSrc(this.defaultSrc);

// 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.
*/
protected _updateSrcFor() {
if (this._mqActivation) {
let url = this._mqActivation.activatedInput || '';
this._renderer.setAttribute(this.nativeElement, 'src', 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) {
const currentVal = this._queryInput('src');
if (typeof currentVal == 'undefined') {
this._cacheInput('src', value || '');
}
}

/**
* Empty values are maintained, undefined values are exposed as ''
*/
protected get defaultSrc(): string {
let attrVal = getDom().getAttribute(this.nativeElement, 'src');
return this._queryInput('src') || attrVal || '';
}
}
Loading

0 comments on commit 15bdecb

Please sign in to comment.