From 22a92e1056cc0d1c9045051f81d3880feaa01f8a Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Sun, 3 Apr 2016 02:18:36 -0400
Subject: [PATCH 09/37] test
---
src/components/icon/icon.spec.ts | 25 +++++++++++++++++++++++++
1 file changed, 25 insertions(+)
create mode 100644 src/components/icon/icon.spec.ts
diff --git a/src/components/icon/icon.spec.ts b/src/components/icon/icon.spec.ts
new file mode 100644
index 000000000000..e3e860c9c79f
--- /dev/null
+++ b/src/components/icon/icon.spec.ts
@@ -0,0 +1,25 @@
+import {
+ inject,
+ TestComponentBuilder
+} from 'angular2/testing';
+import {
+ it,
+ describe,
+ expect,
+ beforeEach,
+} from '../../core/facade/testing';
+import {Component} from 'angular2/core';
+import {By} from 'angular2/platform/browser';
+
+import {MdIcon} from './icon';
+import {MdIconProvider} from './icon-provider';
+
+export function main() {
+ describe('MdIcon', () => {
+ let builder: TestComponentBuilder;
+
+ beforeEach(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
+ builder = tcb;
+ }));
+ });
+}
\ No newline at end of file
From 0e6f396efa7a8bbbc387054c0d73ea85718c2735 Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Mon, 4 Apr 2016 01:22:57 -0400
Subject: [PATCH 10/37] Injecting test HTTP classes.
---
src/components/icon/icon-provider.ts | 5 ++-
src/components/icon/icon.spec.ts | 53 +++++++++++++++++++++++++---
src/core/facade/testing.ts | 1 +
test/karma.config.ts | 2 ++
4 files changed, 56 insertions(+), 5 deletions(-)
diff --git a/src/components/icon/icon-provider.ts b/src/components/icon/icon-provider.ts
index c7ea487bda21..d63d3930e2cc 100644
--- a/src/components/icon/icon-provider.ts
+++ b/src/components/icon/icon-provider.ts
@@ -28,7 +28,7 @@ export class MdIconProvider {
private _defaultViewBoxSize = 24;
private _defaultFontSetClass = 'material-icons';
- constructor(private _http: Http, private _renderer: Renderer) {
+ constructor(private _http: Http) {
}
public registerIcon(name: string, url: string, viewBoxSize:number=0): MdIconProvider {
@@ -127,6 +127,8 @@ export class MdIconProvider {
return this._inProgressUrlFetches.get(url);
}
console.log("*** Sending request");
+ throw Error('oops');
+ /*
const req = this._http.get(url)
.do((response) => {
console.log('*** Removing request: ' + url);
@@ -135,6 +137,7 @@ export class MdIconProvider {
.map((response) => response.text());
this._inProgressUrlFetches.set(url, req);
return req;
+ */
}
private _loadIconFromConfig(config: IconConfig): Observable {
diff --git a/src/components/icon/icon.spec.ts b/src/components/icon/icon.spec.ts
index e3e860c9c79f..61871718f552 100644
--- a/src/components/icon/icon.spec.ts
+++ b/src/components/icon/icon.spec.ts
@@ -1,25 +1,70 @@
import {
inject,
- TestComponentBuilder
+ TestComponentBuilder,
} from 'angular2/testing';
+import {
+ HTTP_PROVIDERS,
+ BaseRequestOptions,
+ Response,
+ ResponseOptions,
+ Http,
+ XHRBackend} from 'angular2/http';
+import {
+ MockBackend,
+ MockConnection} from 'angular2/http/testing';
import {
it,
describe,
+ ddescribe,
expect,
beforeEach,
+ beforeEachProviders,
} from '../../core/facade/testing';
-import {Component} from 'angular2/core';
+import {
+ provide,
+ Component} from 'angular2/core';
import {By} from 'angular2/platform/browser';
+import {
+ TEST_BROWSER_PLATFORM_PROVIDERS,
+ TEST_BROWSER_APPLICATION_PROVIDERS
+} from 'angular2/platform/testing/browser';
import {MdIcon} from './icon';
import {MdIconProvider} from './icon-provider';
export function main() {
- describe('MdIcon', () => {
+ ddescribe('MdIcon', () => {
+
+ beforeEachProviders(() => [
+ MdIconProvider,
+ HTTP_PROVIDERS,
+ MockBackend,
+ provide(XHRBackend, {useExisting: MockBackend}),
+ ]);
+
let builder: TestComponentBuilder;
beforeEach(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
builder = tcb;
}));
+
+ it('should add material-icons class by default', (done: () => void) => {
+ console.log('in test?');
+ return builder.createAsync(MdIconLigatureTestApp).then((fixture) => {
+ const testComponent = fixture.debugElement.componentInstance;
+ const nativeElement = fixture.debugElement.nativeElement;
+ done();
+ });
+ });
});
-}
\ No newline at end of file
+}
+
+/** Test component that contains an MdIcon. */
+@Component({
+ selector: 'test-app',
+ template: `{{iconName}}`,
+ directives: [MdIcon],
+})
+class MdIconLigatureTestApp {
+ iconName = 'home';
+}
diff --git a/src/core/facade/testing.ts b/src/core/facade/testing.ts
index 7c7d80b63a05..e5b7370796f1 100644
--- a/src/core/facade/testing.ts
+++ b/src/core/facade/testing.ts
@@ -5,4 +5,5 @@ export {
ddescribe,
expect,
beforeEach,
+ beforeEachProviders,
} from 'angular2/testing';
diff --git a/test/karma.config.ts b/test/karma.config.ts
index a89437b2dd8d..f57b5b9b3505 100644
--- a/test/karma.config.ts
+++ b/test/karma.config.ts
@@ -31,6 +31,8 @@ export function config(config) {
'node_modules/reflect-metadata/Reflect.js',
{pattern: 'node_modules/angular2/bundles/angular2.dev.js', included: true, watched: true},
{pattern: 'node_modules/angular2/bundles/testing.dev.js', included: true, watched: true},
+ {pattern: 'node_modules/angular2/bundles/http.dev.js', included: true, watched: true},
+ //{pattern: 'node_modules/angular2/http/testing.js', included: true, watched: true},
{pattern: 'test/karma-test-shim.js', included: true, watched: true},
From 014be776408afd6958d54d6ab55287b87a00f86e Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Mon, 4 Apr 2016 02:48:47 -0400
Subject: [PATCH 11/37] start of tests
---
src/components/icon/icon-provider.ts | 3 --
src/components/icon/icon.spec.ts | 45 +++++++++++++++++++++++-----
src/components/icon/icon.ts | 27 +++++++++++++++--
src/core/facade/testing.ts | 1 +
4 files changed, 64 insertions(+), 12 deletions(-)
diff --git a/src/components/icon/icon-provider.ts b/src/components/icon/icon-provider.ts
index d63d3930e2cc..1af21885f9e8 100644
--- a/src/components/icon/icon-provider.ts
+++ b/src/components/icon/icon-provider.ts
@@ -127,8 +127,6 @@ export class MdIconProvider {
return this._inProgressUrlFetches.get(url);
}
console.log("*** Sending request");
- throw Error('oops');
- /*
const req = this._http.get(url)
.do((response) => {
console.log('*** Removing request: ' + url);
@@ -137,7 +135,6 @@ export class MdIconProvider {
.map((response) => response.text());
this._inProgressUrlFetches.set(url, req);
return req;
- */
}
private _loadIconFromConfig(config: IconConfig): Observable {
diff --git a/src/components/icon/icon.spec.ts b/src/components/icon/icon.spec.ts
index 61871718f552..65436cead696 100644
--- a/src/components/icon/icon.spec.ts
+++ b/src/components/icon/icon.spec.ts
@@ -19,6 +19,7 @@ import {
expect,
beforeEach,
beforeEachProviders,
+ xit,
} from '../../core/facade/testing';
import {
provide,
@@ -32,6 +33,8 @@ import {
import {MdIcon} from './icon';
import {MdIconProvider} from './icon-provider';
+const sortedClassNames = (elem: Element) => elem.className.split(' ').sort();
+
export function main() {
ddescribe('MdIcon', () => {
@@ -48,12 +51,39 @@ export function main() {
builder = tcb;
}));
- it('should add material-icons class by default', (done: () => void) => {
- console.log('in test?');
- return builder.createAsync(MdIconLigatureTestApp).then((fixture) => {
- const testComponent = fixture.debugElement.componentInstance;
- const nativeElement = fixture.debugElement.nativeElement;
- done();
+ describe('Ligature icons', () => {
+ it('should add material-icons class by default', (done: () => void) => {
+ return builder.createAsync(MdIconLigatureTestApp).then((fixture) => {
+ const testComponent = fixture.debugElement.componentInstance;
+ const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
+ fixture.detectChanges();
+ expect(sortedClassNames(mdIconElement)).toEqual(['material-icons']);
+ done();
+ });
+ });
+
+ // This test is disabled because the DOM in the test environment can't read
+ // the text content of the md-icon element.
+ xit('should set aria label from text content if not specified', (done: () => void) => {
+ return builder.createAsync(MdIconLigatureTestApp).then((fixture) => {
+ const testComponent = fixture.debugElement.componentInstance;
+ const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
+ fixture.detectChanges();
+ expect(mdIconElement.getAttribute('aria-label')).toBe('home');
+ done();
+ });
+ });
+
+ // And getAttribute doesn't see values set by Renderer.setElementAttribute?
+ xit('should use provided aria label', (done: () => void) => {
+ return builder.createAsync(MdIconLigatureTestApp).then((fixture) => {
+ const testComponent = fixture.debugElement.componentInstance;
+ const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
+ testComponent.ariaLabel = 'house';
+ fixture.detectChanges();
+ expect(mdIconElement.getAttribute('aria-label')).toBe('house');
+ done();
+ });
});
});
});
@@ -62,9 +92,10 @@ export function main() {
/** Test component that contains an MdIcon. */
@Component({
selector: 'test-app',
- template: `{{iconName}}`,
+ template: `{{iconName}}`,
directives: [MdIcon],
})
class MdIconLigatureTestApp {
+ ariaLabel: string = null;
iconName = 'home';
}
diff --git a/src/components/icon/icon.ts b/src/components/icon/icon.ts
index bd4cedc7f460..c77812835e6a 100644
--- a/src/components/icon/icon.ts
+++ b/src/components/icon/icon.ts
@@ -1,4 +1,5 @@
import {
+ ChangeDetectorRef,
Component,
Directive,
ElementRef,
@@ -23,7 +24,6 @@ import {
styleUrls: ['./components/icon/icon.css'],
host: {
'role': 'img',
- '[attr.aria-label]': 'ariaLabel()',
},
directives: [NgClass],
encapsulation: ViewEncapsulation.None,
@@ -43,6 +43,7 @@ export class MdIcon implements OnChanges, OnInit {
constructor(
private _element: ElementRef,
private _renderer: Renderer,
+ private _changeDetectorRef: ChangeDetectorRef,
private _mdIconProvider: MdIconProvider) {
}
@@ -67,15 +68,34 @@ export class MdIcon implements OnChanges, OnInit {
if (this._usingFontIcon()) {
this._updateFontIconClasses();
}
+
+ this._updateAriaLabel();
}
ngOnInit() {
+ // Update font classes and aria attribute here because ngOnChanges won't be called
+ // if none of the inputs are present. That can happen for a ligature icon like
+ // home, in which case we need to set the default icon font and
+ // get the aria label from the text content.
if (this._usingFontIcon()) {
this._updateFontIconClasses();
}
+ this._updateAriaLabel();
+ }
+
+ private _updateAriaLabel() {
+ console.log('aria from parent: ' + this.ariaLabelFromParent);
+ if (!this.ariaLabelFromParent) {
+ const ariaLabel = this._getAriaLabel();
+ console.log('Got aria label: ' + ariaLabel);
+ if (ariaLabel) {
+ this._renderer.setElementAttribute(this._element.nativeElement, 'aria-label', ariaLabel);
+ this._changeDetectorRef.detectChanges();
+ }
+ }
}
- ariaLabel() {
+ private _getAriaLabel() {
// If the parent provided an aria-label attribute value, use it as-is. Otherwise look for a
// reasonable value from the alt attribute, font icon name, SVG icon name, or (for ligatures)
// the text content of the directive.
@@ -110,6 +130,9 @@ export class MdIcon implements OnChanges, OnInit {
}
private _updateFontIconClasses() {
+ if (!this._usingFontIcon()) {
+ return;
+ }
const elem = this._element.nativeElement;
const fontSetClass = this.fontSet ?
this._mdIconProvider.classNameForFontAlias(this.fontSet) :
diff --git a/src/core/facade/testing.ts b/src/core/facade/testing.ts
index e5b7370796f1..77fff8359783 100644
--- a/src/core/facade/testing.ts
+++ b/src/core/facade/testing.ts
@@ -1,6 +1,7 @@
export {
it,
iit,
+ xit,
describe,
ddescribe,
expect,
From ea3beea9783755c2b2e062c094965af12f139b1b Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Tue, 5 Apr 2016 00:56:55 -0400
Subject: [PATCH 12/37] HTTP tests.
---
package.json | 2 +-
src/components/icon/icon.spec.ts | 169 ++++++++++++++++++++++++++++++-
2 files changed, 167 insertions(+), 4 deletions(-)
diff --git a/package.json b/package.json
index ae1566bcd647..d0a47c3ba4de 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,7 @@
},
"devDependencies": {
"add-stream": "^1.0.0",
- "angular-cli": "^0.0.28",
+ "angular-cli": "^0.0.29",
"angular-cli-github-pages": "^0.2.0",
"broccoli-autoprefixer": "^4.1.0",
"broccoli-caching-writer": "^2.2.1",
diff --git a/src/components/icon/icon.spec.ts b/src/components/icon/icon.spec.ts
index 65436cead696..9ecb672dd741 100644
--- a/src/components/icon/icon.spec.ts
+++ b/src/components/icon/icon.spec.ts
@@ -2,6 +2,7 @@ import {
inject,
TestComponentBuilder,
} from 'angular2/testing';
+
import {
HTTP_PROVIDERS,
BaseRequestOptions,
@@ -36,7 +37,7 @@ import {MdIconProvider} from './icon-provider';
const sortedClassNames = (elem: Element) => elem.className.split(' ').sort();
export function main() {
- ddescribe('MdIcon', () => {
+ describe('MdIcon', () => {
beforeEachProviders(() => [
MdIconProvider,
@@ -46,9 +47,41 @@ export function main() {
]);
let builder: TestComponentBuilder;
+ let mdIconProvider: MdIconProvider;
- beforeEach(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
+ beforeEach(
+ inject([TestComponentBuilder, MdIconProvider, MockBackend],
+ (tcb: TestComponentBuilder, mip: MdIconProvider, mockBackend: MockBackend) => {
builder = tcb;
+ mdIconProvider = mip;
+ // Set fake responses for various SVG URLs.
+ mockBackend.connections.subscribe((connection: any) => {
+ switch (connection.request.url) {
+ case 'cat.svg':
+ connection.mockRespond(new Response(new ResponseOptions({
+ status: 200,
+ body: '',
+ })));
+ break;
+ case 'dog.svg':
+ connection.mockRespond(new Response(new ResponseOptions({
+ status: 200,
+ body: '',
+ })));
+ break;
+ case 'farm-set.svg':
+ connection.mockRespond(new Response(new ResponseOptions({
+ status: 200,
+ body: `
+
+
+
+
+ `,
+ })));
+ break;
+ }
+ });
}));
describe('Ligature icons', () => {
@@ -56,6 +89,7 @@ export function main() {
return builder.createAsync(MdIconLigatureTestApp).then((fixture) => {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
+ testComponent.iconName = 'home';
fixture.detectChanges();
expect(sortedClassNames(mdIconElement)).toEqual(['material-icons']);
done();
@@ -68,6 +102,7 @@ export function main() {
return builder.createAsync(MdIconLigatureTestApp).then((fixture) => {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
+ testComponent.iconName = 'home';
fixture.detectChanges();
expect(mdIconElement.getAttribute('aria-label')).toBe('home');
done();
@@ -79,6 +114,7 @@ export function main() {
return builder.createAsync(MdIconLigatureTestApp).then((fixture) => {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
+ testComponent.iconName = 'home';
testComponent.ariaLabel = 'house';
fixture.detectChanges();
expect(mdIconElement.getAttribute('aria-label')).toBe('house');
@@ -86,6 +122,113 @@ export function main() {
});
});
});
+
+ describe('Icons from URLs', () => {
+ it('should fetch SVG icon from URL and inline the content', (done: () => void) => {
+ return builder.createAsync(MdIconFromSvgUrlTestApp).then((fixture) => {
+ const testComponent = fixture.debugElement.componentInstance;
+ const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
+ testComponent.iconUrl = 'cat.svg';
+ fixture.detectChanges();
+
+ // An element should have been added as a child of .
+ expect(mdIconElement.children.length).toBe(1);
+ let svgElement = mdIconElement.children[0];
+ expect(svgElement.tagName.toLowerCase()).toBe('svg');
+ // Default attributes should be set.
+ expect(svgElement.getAttribute('height')).toBe('100%');
+ expect(svgElement.getAttribute('height')).toBe('100%');
+ expect(svgElement.getAttribute('viewBox')).toBe('0 0 24 24');
+ // Make sure SVG content is taken from response.
+ expect(svgElement.children.length).toBe(1);
+ let pathElement = svgElement.children[0];
+ expect(pathElement.tagName.toLowerCase()).toBe('path');
+ expect(pathElement.getAttribute('d')).toBe('meow');
+
+ // Change the icon, and the SVG element should be replaced.
+ testComponent.iconUrl = 'dog.svg';
+ fixture.detectChanges();
+ expect(mdIconElement.children.length).toBe(1);
+ svgElement = mdIconElement.children[0];
+ expect(svgElement.tagName.toLowerCase()).toBe('svg');
+ expect(svgElement.children.length).toBe(1);
+ pathElement = svgElement.children[0];
+ expect(pathElement.tagName.toLowerCase()).toBe('path');
+ expect(pathElement.getAttribute('d')).toBe('woof');
+
+ done();
+ });
+ });
+
+ it('should register icon URLs by name', (done: () => void) => {
+ mdIconProvider.registerIcon('fluffy', 'cat.svg');
+ mdIconProvider.registerIcon('fido', 'dog.svg');
+ return builder.createAsync(MdIconFromSvgNameTestApp).then((fixture) => {
+ const testComponent = fixture.debugElement.componentInstance;
+ const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
+ testComponent.iconName= 'fido';
+ fixture.detectChanges();
+ expect(mdIconElement.children.length).toBe(1);
+ let svgElement = mdIconElement.children[0];
+ expect(svgElement.tagName.toLowerCase()).toBe('svg');
+ expect(svgElement.children.length).toBe(1);
+ let pathElement = svgElement.children[0];
+ expect(pathElement.tagName.toLowerCase()).toBe('path');
+ expect(pathElement.getAttribute('d')).toBe('woof');
+
+ // Change the icon, and the SVG element should be replaced.
+ testComponent.iconName = 'fluffy';
+ fixture.detectChanges();
+ expect(mdIconElement.children.length).toBe(1);
+ svgElement = mdIconElement.children[0];
+ expect(svgElement.tagName.toLowerCase()).toBe('svg');
+ expect(svgElement.children.length).toBe(1);
+ pathElement = svgElement.children[0];
+ expect(pathElement.tagName.toLowerCase()).toBe('path');
+ expect(pathElement.getAttribute('d')).toBe('meow');
+
+ done();
+ });
+ });
+
+ it('should extract icon from SVG icon set', (done: () => void) => {
+ mdIconProvider.registerIconSet('farm', 'farm-set.svg');
+ return builder.createAsync(MdIconFromSvgNameTestApp).then((fixture) => {
+ const testComponent = fixture.debugElement.componentInstance;
+ const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
+ testComponent.iconName = 'farm:pig';
+ fixture.detectChanges();
+ expect(mdIconElement.children.length).toBe(1);
+ let svgElement = mdIconElement.children[0];
+ expect(svgElement.tagName.toLowerCase()).toBe('svg');
+ expect(svgElement.children.length).toBe(1);
+ let svgChild = svgElement.children[0];
+ // The first child should be the element.
+ expect(svgChild.tagName.toLowerCase()).toBe('g');
+ expect(svgChild.getAttribute('id')).toBe('pig');
+ expect(svgChild.children.length).toBe(1);
+ let pathElement = svgChild.children[0];
+ expect(pathElement.tagName.toLowerCase()).toBe('path');
+ expect(pathElement.getAttribute('d')).toBe('oink');
+
+ // Change the icon, and the SVG element should be replaced.
+ testComponent.iconName = 'farm:cow';
+ fixture.detectChanges();
+ expect(mdIconElement.children.length).toBe(1);
+ svgElement = mdIconElement.children[0];
+ svgChild = svgElement.children[0];
+ // The first child should be the element.
+ expect(svgChild.tagName.toLowerCase()).toBe('g');
+ expect(svgChild.getAttribute('id')).toBe('cow');
+ expect(svgChild.children.length).toBe(1);
+ pathElement = svgChild.children[0];
+ expect(pathElement.tagName.toLowerCase()).toBe('path');
+ expect(pathElement.getAttribute('d')).toBe('moo');
+
+ done();
+ });
+ });
+ });
});
}
@@ -97,5 +240,25 @@ export function main() {
})
class MdIconLigatureTestApp {
ariaLabel: string = null;
- iconName = 'home';
+ iconName = '';
+}
+
+@Component({
+ selector: 'test-app',
+ template: ``,
+ directives: [MdIcon],
+})
+class MdIconFromSvgUrlTestApp {
+ ariaLabel: string = null;
+ iconUrl = '';
+}
+
+@Component({
+ selector: 'test-app',
+ template: ``,
+ directives: [MdIcon],
+})
+class MdIconFromSvgNameTestApp {
+ ariaLabel: string = null;
+ iconName = '';
}
From 72d167f7108472e2fd69de6d8c594b7a09e8d004 Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Thu, 7 Apr 2016 01:46:05 -0400
Subject: [PATCH 13/37] Improved change detection and added more tests.
---
src/components/icon/icon.spec.ts | 75 ++++++++++++++++++++++++++++----
src/components/icon/icon.ts | 59 ++++++++++++++++---------
2 files changed, 104 insertions(+), 30 deletions(-)
diff --git a/src/components/icon/icon.spec.ts b/src/components/icon/icon.spec.ts
index 9ecb672dd741..d1c84f37d2cb 100644
--- a/src/components/icon/icon.spec.ts
+++ b/src/components/icon/icon.spec.ts
@@ -14,7 +14,7 @@ import {
MockBackend,
MockConnection} from 'angular2/http/testing';
import {
- it,
+ it, iit,
describe,
ddescribe,
expect,
@@ -96,22 +96,24 @@ export function main() {
});
});
- // This test is disabled because the DOM in the test environment can't read
- // the text content of the md-icon element.
- xit('should set aria label from text content if not specified', (done: () => void) => {
+ it('should set aria label from text content if not specified', (done: () => void) => {
return builder.createAsync(MdIconLigatureTestApp).then((fixture) => {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
testComponent.iconName = 'home';
fixture.detectChanges();
expect(mdIconElement.getAttribute('aria-label')).toBe('home');
+
+ testComponent.iconName = 'hand';
+ fixture.detectChanges();
+ expect(mdIconElement.getAttribute('aria-label')).toBe('hand');
+
done();
});
});
- // And getAttribute doesn't see values set by Renderer.setElementAttribute?
- xit('should use provided aria label', (done: () => void) => {
- return builder.createAsync(MdIconLigatureTestApp).then((fixture) => {
+ it('should use provided aria label', (done: () => void) => {
+ return builder.createAsync(MdIconLigatureWithAriaBindingTestApp).then((fixture) => {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
testComponent.iconName = 'home';
@@ -175,6 +177,9 @@ export function main() {
let pathElement = svgElement.children[0];
expect(pathElement.tagName.toLowerCase()).toBe('path');
expect(pathElement.getAttribute('d')).toBe('woof');
+ // The aria label should be taken from the icon name.
+ expect(mdIconElement.getAttribute('aria-label')).toBe('fido');
+
// Change the icon, and the SVG element should be replaced.
testComponent.iconName = 'fluffy';
@@ -186,6 +191,7 @@ export function main() {
pathElement = svgElement.children[0];
expect(pathElement.tagName.toLowerCase()).toBe('path');
expect(pathElement.getAttribute('d')).toBe('meow');
+ expect(mdIconElement.getAttribute('aria-label')).toBe('fluffy');
done();
});
@@ -210,6 +216,8 @@ export function main() {
let pathElement = svgChild.children[0];
expect(pathElement.tagName.toLowerCase()).toBe('path');
expect(pathElement.getAttribute('d')).toBe('oink');
+ // The aria label should be taken from the icon name (without the icon set portion).
+ expect(mdIconElement.getAttribute('aria-label')).toBe('pig');
// Change the icon, and the SVG element should be replaced.
testComponent.iconName = 'farm:cow';
@@ -224,18 +232,43 @@ export function main() {
pathElement = svgChild.children[0];
expect(pathElement.tagName.toLowerCase()).toBe('path');
expect(pathElement.getAttribute('d')).toBe('moo');
+ expect(mdIconElement.getAttribute('aria-label')).toBe('cow');
done();
});
});
});
+
+ describe('custom fonts', () => {
+ it('should apply CSS classes for custom font and icon', (done: () => void) => {
+ mdIconProvider.registerFontSet('f1', 'font1');
+ mdIconProvider.registerFontSet('f2');
+ return builder.createAsync(MdIconCustomFontCssTestApp).then((fixture) => {
+ const testComponent = fixture.debugElement.componentInstance;
+ const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
+ testComponent.fontSet = 'f1';
+ testComponent.fontIcon = 'house';
+ fixture.detectChanges();
+ expect(sortedClassNames(mdIconElement)).toEqual(['font1', 'house']);
+ expect(mdIconElement.getAttribute('aria-label')).toBe('house');
+
+ testComponent.fontSet = 'f2';
+ testComponent.fontIcon = 'igloo';
+ fixture.detectChanges();
+ expect(sortedClassNames(mdIconElement)).toEqual(['f2', 'igloo']);
+ expect(mdIconElement.getAttribute('aria-label')).toBe('igloo');
+
+ done();
+ });
+ });
+ });
});
}
-/** Test component that contains an MdIcon. */
+/** Test components that contain an MdIcon. */
@Component({
selector: 'test-app',
- template: `{{iconName}}`,
+ template: `{{iconName}}`,
directives: [MdIcon],
})
class MdIconLigatureTestApp {
@@ -243,6 +276,30 @@ class MdIconLigatureTestApp {
iconName = '';
}
+@Component({
+ selector: 'test-app',
+ template: `{{iconName}}`,
+ directives: [MdIcon],
+})
+class MdIconLigatureWithAriaBindingTestApp {
+ ariaLabel: string = null;
+ iconName = '';
+}
+
+@Component({
+ selector: 'test-app',
+ template: `
+
+ `,
+ directives: [MdIcon],
+})
+class MdIconCustomFontCssTestApp {
+ ariaLabel: string = null;
+ fontSet = '';
+ fontIcon = '';
+}
+
+
@Component({
selector: 'test-app',
template: ``,
diff --git a/src/components/icon/icon.ts b/src/components/icon/icon.ts
index c77812835e6a..7ffb1bcbbca8 100644
--- a/src/components/icon/icon.ts
+++ b/src/components/icon/icon.ts
@@ -1,4 +1,5 @@
import {
+ AfterContentChecked,
ChangeDetectorRef,
Component,
Directive,
@@ -28,7 +29,7 @@ import {
directives: [NgClass],
encapsulation: ViewEncapsulation.None,
})
-export class MdIcon implements OnChanges, OnInit {
+export class MdIcon implements OnChanges, OnInit, AfterContentChecked {
@Input() svgSrc: string;
@Input() svgIcon: string;
@Input() fontSet: string;
@@ -46,19 +47,37 @@ export class MdIcon implements OnChanges, OnInit {
private _changeDetectorRef: ChangeDetectorRef,
private _mdIconProvider: MdIconProvider) {
}
-
+
+ /**
+ * Splits an svgIcon binding value into its icon set and icon name components.
+ * Returns a 2-element array of [(icon set), (icon name)].
+ * The separator for the two fields is ':'. If there is no separator, an empty
+ * string is returned for the icon set and the entire value is returned for
+ * the icon name. If the argument is falsy, returns an array of two empty strings.
+ * Examples:
+ * 'social:cake' -> ['social', 'cake']
+ * 'penguin' -> ['', 'penguin']
+ * null -> ['', '']
+ */
+ private _splitIconName(iconName: string): [string, string] {
+ if (!iconName) {
+ return ['', ''];
+ }
+ const sepIndex = this.svgIcon.indexOf(':');
+ if (sepIndex == -1) {
+ return ['', iconName];
+ }
+ return [iconName.substring(0, sepIndex), iconName.substring(sepIndex + 1)];
+ }
+
ngOnChanges(changes: {[propertyName: string]: SimpleChange}) {
if (this.svgIcon) {
- const sepIndex = this.svgIcon.indexOf(':');
- if (sepIndex == -1) {
- // Icon not in set.
- this._mdIconProvider.loadIconByName(this.svgIcon)
+ const [iconSet, iconName] = this._splitIconName(this.svgIcon);
+ if (iconSet) {
+ this._mdIconProvider.loadIconFromSetByName(iconSet, iconName)
.subscribe((svg: SVGElement) => this._setSvgElement(svg));
} else {
- // Set name is before separator.
- const setName = this.svgIcon.substring(0, sepIndex);
- const iconName = this.svgIcon.substring(sepIndex + 1);
- this._mdIconProvider.loadIconFromSetByName(setName, iconName)
+ this._mdIconProvider.loadIconByName(this.svgIcon)
.subscribe((svg: SVGElement) => this._setSvgElement(svg));
}
} else if (this.svgSrc) {
@@ -68,31 +87,29 @@ export class MdIcon implements OnChanges, OnInit {
if (this._usingFontIcon()) {
this._updateFontIconClasses();
}
-
this._updateAriaLabel();
}
-
+
ngOnInit() {
- // Update font classes and aria attribute here because ngOnChanges won't be called
- // if none of the inputs are present. That can happen for a ligature icon like
- // home, in which case we need to set the default icon font and
- // get the aria label from the text content.
+ // Update font classes because ngOnChanges won't be called if none of the inputs are present,
+ // e.g. arrow. In this case we need to add a CSS class for the default font.
if (this._usingFontIcon()) {
this._updateFontIconClasses();
}
+ }
+
+ ngAfterContentChecked() {
+ // Update aria label here because it may depend on the projected text content.
+ // (e.g. home should use 'home').
this._updateAriaLabel();
}
private _updateAriaLabel() {
- console.log('aria from parent: ' + this.ariaLabelFromParent);
- if (!this.ariaLabelFromParent) {
const ariaLabel = this._getAriaLabel();
- console.log('Got aria label: ' + ariaLabel);
if (ariaLabel) {
this._renderer.setElementAttribute(this._element.nativeElement, 'aria-label', ariaLabel);
this._changeDetectorRef.detectChanges();
}
- }
}
private _getAriaLabel() {
@@ -103,7 +120,7 @@ export class MdIcon implements OnChanges, OnInit {
this.ariaLabelFromParent ||
this.alt ||
this.fontIcon ||
- this.svgIcon;
+ this._splitIconName(this.svgIcon)[1];
if (label) {
return label;
}
From 53aeed227b752a5e96a8c0fac57d50f7e9977a8c Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Fri, 8 Apr 2016 03:32:26 -0400
Subject: [PATCH 14/37] Icon namespaces, addIcon and addIconSet are now
additive.
---
src/components/icon/icon-provider.ts | 202 ++++++++++++++++++++-------
src/components/icon/icon.spec.ts | 7 +-
src/components/icon/icon.ts | 16 +--
src/demo-app/icon/icon-demo.ts | 4 +-
4 files changed, 169 insertions(+), 60 deletions(-)
diff --git a/src/components/icon/icon-provider.ts b/src/components/icon/icon-provider.ts
index 1af21885f9e8..4cedf77b6301 100644
--- a/src/components/icon/icon-provider.ts
+++ b/src/components/icon/icon-provider.ts
@@ -1,24 +1,30 @@
import {Injectable, Renderer} from 'angular2/core';
import {Http, Response, HTTP_PROVIDERS} from 'angular2/http';
-import {Observable} from 'rxjs/Rx';
+import {AsyncSubject, Observer, Observable} from 'rxjs/Rx';
/**
- * Configuration for a named icon, used when initially loading it.
+ * Configuration for an icon, possibly including the cached SVG element.
*/
class IconConfig {
+ svgElement: SVGElement = null;
constructor(public url: string, public viewBoxSize: number) {
}
}
@Injectable()
export class MdIconProvider {
- private _iconConfigsByName = new Map();
- private _cachedIconsByName = new Map();
+ // Cache all the things.
- private _iconSetConfigsByName = new Map();
- private _cachedIconSets = new Map();
- // Keys here are "[setName]:[iconName]"
- private _cachedIconsBySetAndName = new Map();
+ // IconConfig objects and cached SVG elements for individual icons.
+ // First level of cache is icon set (which is the empty string for the default set).
+ // Second level is the icon name within the set.
+ private _iconConfigs = new Map>();
+
+ // IconConfig objects and cached SVG elements for icon sets.
+ // These are stored only by set name, but multiple URLs can be registered under the same name.
+ private _iconSetConfigs = new Map();
+
+ // Cache for icons loaded by direct URLs.
private _cachedIconsByUrl = new Map();
private _fontClassNamesByAlias = new Map();
@@ -31,14 +37,30 @@ export class MdIconProvider {
constructor(private _http: Http) {
}
- public registerIcon(name: string, url: string, viewBoxSize:number=0): MdIconProvider {
- this._iconConfigsByName.set(name, new IconConfig(url, viewBoxSize || this._defaultViewBoxSize));
+ public addIcon(iconName: string, url: string, viewBoxSize:number=0): MdIconProvider {
+ return this.addIconInSet('', iconName, url, viewBoxSize);
+ }
+
+ public addIconInSet(
+ setName: string, iconName: string, url: string, viewBoxSize:number=0): MdIconProvider {
+ let iconSetMap = this._iconConfigs.get(setName);
+ if (!iconSetMap) {
+ iconSetMap = new Map();
+ this._iconConfigs.set(setName, iconSetMap);
+ }
+ iconSetMap.set(iconName, new IconConfig(url, viewBoxSize || this._defaultViewBoxSize));
return this;
}
- public registerIconSet(name: string, url: string, viewBoxSize=0): MdIconProvider {
- this._iconSetConfigsByName.set(
- name, new IconConfig(url, viewBoxSize || this._defaultViewBoxSize));
+ public addIconSet(setName: string, url: string, viewBoxSize=0): MdIconProvider {
+ const config = new IconConfig(url, viewBoxSize || this._defaultViewBoxSize);
+ if (this._iconSetConfigs.has(setName)) {
+ // TODO: Allow multiple icon sets.
+ throw Error('Attempted to add multiple icon sets for: ' + setName);
+ // this._iconSetConfigsByName.get(iconSet).push(config);
+ } else {
+ this._iconSetConfigs.set(setName, [config]);
+ }
return this;
}
@@ -65,41 +87,126 @@ export class MdIconProvider {
return this._defaultFontSetClass;
}
- loadIconByName(iconName: string): Observable {
- if (this._cachedIconsByName.has(iconName)) {
- // Copy the element so changes won't affect the original.
- return Observable.of(this._cachedIconsByName.get(iconName).cloneNode(true));
- }
- const iconConfig = this._iconConfigsByName.get(iconName);
- if (!iconConfig) {
- throw Error('Unknown icon: ' + iconName);
- }
- return this._loadIconFromConfig(iconConfig)
- .do((svg: SVGElement) => this._cachedIconsByName.set(iconName, svg));
- }
-
loadIconFromSetByName(setName: string, iconName: string): Observable {
- const combinedKey = setName + ':' + iconName;
- // Check for this specific icon being cached.
- if (this._cachedIconsBySetAndName.has(combinedKey)) {
- return Observable.of(this._cachedIconsBySetAndName.get(combinedKey).cloneNode(true));
- }
- const iconConfig = this._iconSetConfigsByName.get(setName);
- if (!iconConfig) {
- throw Error('Unknown icon set: ' + setName);
+ // Return (copy of) cached icon if possible.
+ if (this._iconConfigs.has(setName) && this._iconConfigs.get(setName).has(iconName)) {
+ const config = this._iconConfigs.get(setName).get(iconName);
+ if (config.svgElement) {
+ // We already have the SVG element for this icon, return a copy.
+ return Observable.of(config.svgElement.cloneNode(true));
+ } else {
+ // Fetch the icon from the config's URL, cache it, and return a copy.
+ return this._loadIconFromConfig(config)
+ .do((svg: SVGElement) => config.svgElement = svg)
+ .map((svg: SVGElement) => svg.cloneNode(true));
+ }
}
- // Check for the icon set being cached.
- if (this._cachedIconSets.has(setName)) {
- var iconFromSet = this._extractSvgIconFromSet(
- this._cachedIconSets.get(setName), iconName, iconConfig);
- this._cachedIconsBySetAndName.set(combinedKey, iconFromSet);
- return Observable.of(iconFromSet.cloneNode(true));
+ // See if we have any icon sets registered for the set name.
+ const iconSetConfigs = this._iconSetConfigs.get(setName);
+ if (iconSetConfigs) {
+ const unfetchedIconSetConfigs = <[IconConfig]>[];
+ // For all the icon set SVG elements we've fetched, see if any contain an icon with the
+ // requested name.
+ for (const setConfig of iconSetConfigs) {
+ if (setConfig.svgElement) {
+ const namedIcon = this._extractSvgIconFromSet(setConfig.svgElement, iconName, setConfig);
+ if (namedIcon) {
+ // We could cache namedSvg in _iconConfigs, but since we have to make a copy every
+ // time anyway, there's probably not much advantage compared to just always extracting
+ // it from the icon set.
+ return Observable.of(namedIcon.cloneNode(true));
+ }
+ } else {
+ unfetchedIconSetConfigs.push(setConfig);
+ }
+ }
+ // Not found in any cached icon sets. If there are icon sets with URLs that we haven't
+ // fetched, fetch them now and look for iconName in the results.
+ if (unfetchedIconSetConfigs.length) {
+ // The fun part. We need to asynchronously fetch the URLs, and see if any of them
+ // have an icon with the requested name.
+ let foundIcon: SVGElement = null;
+
+ let parallelHttpRequests = unfetchedIconSetConfigs.map((setConfig) => {
+ return this._loadIconSetFromConfig(setConfig)
+ .catch((err: any, source: any, caught: any): Observable => {
+ // Swallow errors fetching individual URLs so the combined Observable won't
+ // necessarily fail.
+ console.log(`Loading icon set URL: ${setConfig.url} failed with error: ${err}`);
+ return Observable.of(null);
+ })
+ .do((svg: SVGElement) => {
+ // Cache SVG element and look for named icon if we haven't already found it.
+ if (svg) {
+ setConfig.svgElement = svg;
+ }
+ if (!foundIcon) {
+ foundIcon = this._extractSvgIconFromSet(svg, iconName, setConfig);
+ }
+ });
+ });
+ // This will wait for all the URLs to come back. Ideally we'd like to return as soon as we
+ // find an icon with the correct name.
+ return Observable.forkJoin(parallelHttpRequests)
+ .map((ignoredResults: any) => {
+ if (foundIcon) {
+ return foundIcon;
+ }
+ throw Error(`Failed to find icon name: ${iconName} in set: ${setName}`);
+ });
+ /* // Trying to use AsyncSubject, which isn't working. The URL never gets fetched.
+ const subject = new AsyncSubject();
+ let responsesReceived = 0;
+
+ console.log('AAA');
+ for (const setConfig of unfetchedIconSetConfigs) {
+ console.log('BBB: ' + setConfig.url);
+ this._loadIconSetFromConfig(setConfig)
+ .do((svg: SVGElement) => {
+ // Cache the element.
+ console.log(`Got icon set svg: ${svg}`);
+ setConfig.svgElement = svg;
+ if (!foundIcon) {
+ const namedIcon = this._extractSvgIconFromSet(svg, iconName, setConfig);
+ if (namedIcon) {
+ // Avoid broadcasting multiple items if more than one set has a matching icon.
+ foundIcon = true;
+ subject.next(namedIcon);
+ }
+ }
+ responsesReceived += 1;
+ if (!foundIcon && responsesReceived == unfetchedIconSetConfigs.length) {
+ subject.error(`Failed to find icon name: ${iconName} in set: ${setName}`);
+ }
+ })
+ .catch((err: any, source: Observable, caught: any): Observable => {
+ console.log(`Loading icon set URL: ${setConfig.url} failed with error: ${err}`);
+ responsesReceived += 1;
+ if (!foundIcon && responsesReceived == unfetchedIconSetConfigs.length) {
+ subject.error(`Failed to find icon name: ${iconName} in set: ${setName}`);
+ }
+ return source;
+ });
+ }
+ return subject;
+ */
+
+ /* // Simple implementation that only supports one icon set.
+ // Fetch the set and extract the icon.
+ const firstConfig = unfetchedIconSetConfigs[0];
+ return this._loadIconSetFromConfig(firstConfig)
+ .do((iconSet: SVGElement) => firstConfig.svgElement = iconSet)
+ .map((iconSet: SVGElement) => {
+ const namedIcon = this._extractSvgIconFromSet(iconSet, iconName, firstConfig);
+ if (!namedIcon) {
+ throw Error(`Failed to find icon name: ${iconName} in set: ${setName}`);
+ }
+ return namedIcon;
+ });
+ */
+ }
}
- // Fetch the set and extract the icon.
- return this._loadIconSetFromConfig(iconConfig)
- .do((iconSet: SVGElement) => this._cachedIconSets.set(setName, iconSet))
- .map((iconSet: SVGElement) =>
- this._extractSvgIconFromSet(iconSet, iconName, iconConfig));
+ return Observable.throw(Error(`Unknown icon name: ${iconName} in set: ${setName}`));
}
loadIconFromUrl(url: string): Observable {
@@ -126,9 +233,10 @@ export class MdIconProvider {
console.log("*** Using existing request");
return this._inProgressUrlFetches.get(url);
}
- console.log("*** Sending request");
+ console.log(`*** Sending request for ${url}`);
const req = this._http.get(url)
.do((response) => {
+ console.log(`*** Got response for ${url}`);
console.log('*** Removing request: ' + url);
this._inProgressUrlFetches.delete(url);
})
@@ -171,7 +279,7 @@ export class MdIconProvider {
iconSet: SVGElement, iconName: string, config: IconConfig): SVGElement {
const iconNode = iconSet.querySelector('#' + iconName);
if (!iconNode) {
- throw Error('No icon found in set with id: ' + iconName);
+ return null;
}
// createElement('SVG') doesn't work as expected; the DOM ends up with
// the correct nodes, but the SVG content doesn't render. Instead we
diff --git a/src/components/icon/icon.spec.ts b/src/components/icon/icon.spec.ts
index d1c84f37d2cb..836e8f913414 100644
--- a/src/components/icon/icon.spec.ts
+++ b/src/components/icon/icon.spec.ts
@@ -55,6 +55,7 @@ export function main() {
builder = tcb;
mdIconProvider = mip;
// Set fake responses for various SVG URLs.
+ // TODO: Keep track of requests so we can verify caching behavior.
mockBackend.connections.subscribe((connection: any) => {
switch (connection.request.url) {
case 'cat.svg':
@@ -163,8 +164,8 @@ export function main() {
});
it('should register icon URLs by name', (done: () => void) => {
- mdIconProvider.registerIcon('fluffy', 'cat.svg');
- mdIconProvider.registerIcon('fido', 'dog.svg');
+ mdIconProvider.addIcon('fluffy', 'cat.svg');
+ mdIconProvider.addIcon('fido', 'dog.svg');
return builder.createAsync(MdIconFromSvgNameTestApp).then((fixture) => {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
@@ -198,7 +199,7 @@ export function main() {
});
it('should extract icon from SVG icon set', (done: () => void) => {
- mdIconProvider.registerIconSet('farm', 'farm-set.svg');
+ mdIconProvider.addIconSet('farm', 'farm-set.svg');
return builder.createAsync(MdIconFromSvgNameTestApp).then((fixture) => {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
diff --git a/src/components/icon/icon.ts b/src/components/icon/icon.ts
index 7ffb1bcbbca8..eb41cb4f9fe9 100644
--- a/src/components/icon/icon.ts
+++ b/src/components/icon/icon.ts
@@ -71,18 +71,18 @@ export class MdIcon implements OnChanges, OnInit, AfterContentChecked {
}
ngOnChanges(changes: {[propertyName: string]: SimpleChange}) {
+ // TODO: Only refetch icons if the values actually changed.
if (this.svgIcon) {
const [iconSet, iconName] = this._splitIconName(this.svgIcon);
- if (iconSet) {
- this._mdIconProvider.loadIconFromSetByName(iconSet, iconName)
- .subscribe((svg: SVGElement) => this._setSvgElement(svg));
- } else {
- this._mdIconProvider.loadIconByName(this.svgIcon)
- .subscribe((svg: SVGElement) => this._setSvgElement(svg));
- }
+ this._mdIconProvider.loadIconFromSetByName(iconSet, iconName)
+ .subscribe(
+ (svg: SVGElement) => this._setSvgElement(svg),
+ (err: any) => console.log(`Error retrieving icon: ${err}`));
} else if (this.svgSrc) {
this._mdIconProvider.loadIconFromUrl(this.svgSrc)
- .subscribe((svg: SVGElement) => this._setSvgElement(svg));
+ .subscribe(
+ (svg: SVGElement) => this._setSvgElement(svg),
+ (err: any) => console.log(`Error retrieving icon: ${err}`));
}
if (this._usingFontIcon()) {
this._updateFontIconClasses();
diff --git a/src/demo-app/icon/icon-demo.ts b/src/demo-app/icon/icon-demo.ts
index 4f5dd7191c69..a262bb901e8e 100644
--- a/src/demo-app/icon/icon-demo.ts
+++ b/src/demo-app/icon/icon-demo.ts
@@ -15,8 +15,8 @@ export class IconDemo {
constructor(mdIconProvider: MdIconProvider) {
mdIconProvider
- .registerIcon('thumb-up', '/demo-app/icon/assets/thumbup-icon.svg')
- .registerIconSet('core', '/demo-app/icon/assets/core-icon-set.svg')
+ .addIcon('thumb-up', '/demo-app/icon/assets/thumbup-icon.svg')
+ .addIconSet('core', '/demo-app/icon/assets/core-icon-set.svg')
.registerFontSet('fontawesome', 'fa');
}
}
From 73900155a68890c2b18a2249d7de14a7c7eadf90 Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Fri, 8 Apr 2016 14:36:07 -0400
Subject: [PATCH 15/37] cleanup
---
src/components/icon/icon-provider.ts | 146 +++++++++------------------
1 file changed, 48 insertions(+), 98 deletions(-)
diff --git a/src/components/icon/icon-provider.ts b/src/components/icon/icon-provider.ts
index 4cedf77b6301..361a807f6c36 100644
--- a/src/components/icon/icon-provider.ts
+++ b/src/components/icon/icon-provider.ts
@@ -13,8 +13,6 @@ class IconConfig {
@Injectable()
export class MdIconProvider {
- // Cache all the things.
-
// IconConfig objects and cached SVG elements for individual icons.
// First level of cache is icon set (which is the empty string for the default set).
// Second level is the icon name within the set.
@@ -55,9 +53,7 @@ export class MdIconProvider {
public addIconSet(setName: string, url: string, viewBoxSize=0): MdIconProvider {
const config = new IconConfig(url, viewBoxSize || this._defaultViewBoxSize);
if (this._iconSetConfigs.has(setName)) {
- // TODO: Allow multiple icon sets.
- throw Error('Attempted to add multiple icon sets for: ' + setName);
- // this._iconSetConfigsByName.get(iconSet).push(config);
+ this._iconSetConfigs.get(setName).push(config);
} else {
this._iconSetConfigs.set(setName, [config]);
}
@@ -104,109 +100,63 @@ export class MdIconProvider {
// See if we have any icon sets registered for the set name.
const iconSetConfigs = this._iconSetConfigs.get(setName);
if (iconSetConfigs) {
- const unfetchedIconSetConfigs = <[IconConfig]>[];
// For all the icon set SVG elements we've fetched, see if any contain an icon with the
// requested name.
- for (const setConfig of iconSetConfigs) {
- if (setConfig.svgElement) {
- const namedIcon = this._extractSvgIconFromSet(setConfig.svgElement, iconName, setConfig);
- if (namedIcon) {
- // We could cache namedSvg in _iconConfigs, but since we have to make a copy every
- // time anyway, there's probably not much advantage compared to just always extracting
- // it from the icon set.
- return Observable.of(namedIcon.cloneNode(true));
- }
- } else {
- unfetchedIconSetConfigs.push(setConfig);
- }
+ const namedIcon = this._extractIconWithNameFromAnySet(iconName, iconSetConfigs);
+ if (namedIcon) {
+ // We could cache namedSvg in _iconConfigs, but since we have to make a copy every
+ // time anyway, there's probably not much advantage compared to just always extracting
+ // it from the icon set.
+ return Observable.of(namedIcon.cloneNode(true));
}
// Not found in any cached icon sets. If there are icon sets with URLs that we haven't
// fetched, fetch them now and look for iconName in the results.
- if (unfetchedIconSetConfigs.length) {
- // The fun part. We need to asynchronously fetch the URLs, and see if any of them
- // have an icon with the requested name.
- let foundIcon: SVGElement = null;
-
- let parallelHttpRequests = unfetchedIconSetConfigs.map((setConfig) => {
- return this._loadIconSetFromConfig(setConfig)
- .catch((err: any, source: any, caught: any): Observable => {
- // Swallow errors fetching individual URLs so the combined Observable won't
- // necessarily fail.
- console.log(`Loading icon set URL: ${setConfig.url} failed with error: ${err}`);
- return Observable.of(null);
- })
- .do((svg: SVGElement) => {
- // Cache SVG element and look for named icon if we haven't already found it.
- if (svg) {
- setConfig.svgElement = svg;
- }
- if (!foundIcon) {
- foundIcon = this._extractSvgIconFromSet(svg, iconName, setConfig);
- }
- });
+ const iconSetFetchRequests = <[Observable]>[];
+ iconSetConfigs.forEach((setConfig) => {
+ if (!setConfig.svgElement) {
+ iconSetFetchRequests.push(
+ this._loadIconSetFromConfig(setConfig)
+ .catch((err: any, source: any, caught: any): Observable => {
+ // Swallow errors fetching individual URLs so the combined Observable won't
+ // necessarily fail.
+ console.log(`Loading icon set URL: ${setConfig.url} failed with error: ${err}`);
+ return Observable.of(null);
+ })
+ .do((svg: SVGElement) => {
+ // Cache SVG element.
+ if (svg) {
+ setConfig.svgElement = svg;
+ }
+ })
+ );
+ }
+ });
+ // Fetch all the icon set URLs. When the requests complete, every IconSet should have a
+ // cached SVG element (unless the request failed), and we can check again for the icon.
+ return Observable.forkJoin(iconSetFetchRequests)
+ .map((ignoredResults: any) => {
+ const foundIcon = this._extractIconWithNameFromAnySet(iconName, iconSetConfigs);
+ if (!foundIcon) {
+ throw Error(`Failed to find icon name: ${iconName} in set: ${setName}`);
+ }
+ return foundIcon;
});
- // This will wait for all the URLs to come back. Ideally we'd like to return as soon as we
- // find an icon with the correct name.
- return Observable.forkJoin(parallelHttpRequests)
- .map((ignoredResults: any) => {
- if (foundIcon) {
- return foundIcon;
- }
- throw Error(`Failed to find icon name: ${iconName} in set: ${setName}`);
- });
- /* // Trying to use AsyncSubject, which isn't working. The URL never gets fetched.
- const subject = new AsyncSubject();
- let responsesReceived = 0;
+ }
+ return Observable.throw(Error(`Unknown icon name: ${iconName} in set: ${setName}`));
+ }
- console.log('AAA');
- for (const setConfig of unfetchedIconSetConfigs) {
- console.log('BBB: ' + setConfig.url);
- this._loadIconSetFromConfig(setConfig)
- .do((svg: SVGElement) => {
- // Cache the element.
- console.log(`Got icon set svg: ${svg}`);
- setConfig.svgElement = svg;
- if (!foundIcon) {
- const namedIcon = this._extractSvgIconFromSet(svg, iconName, setConfig);
- if (namedIcon) {
- // Avoid broadcasting multiple items if more than one set has a matching icon.
- foundIcon = true;
- subject.next(namedIcon);
- }
- }
- responsesReceived += 1;
- if (!foundIcon && responsesReceived == unfetchedIconSetConfigs.length) {
- subject.error(`Failed to find icon name: ${iconName} in set: ${setName}`);
- }
- })
- .catch((err: any, source: Observable, caught: any): Observable => {
- console.log(`Loading icon set URL: ${setConfig.url} failed with error: ${err}`);
- responsesReceived += 1;
- if (!foundIcon && responsesReceived == unfetchedIconSetConfigs.length) {
- subject.error(`Failed to find icon name: ${iconName} in set: ${setName}`);
- }
- return source;
- });
+ private _extractIconWithNameFromAnySet(iconName: string, setConfigs: [IconConfig]): SVGElement {
+ // Iterate backwards, so icon sets added later have precedence.
+ for (let i = setConfigs.length - 1; i >= 0; i--) {
+ const config = setConfigs[i];
+ if (config.svgElement) {
+ const foundIcon = this._extractSvgIconFromSet(config.svgElement, iconName, config);
+ if (foundIcon) {
+ return foundIcon;
}
- return subject;
- */
-
- /* // Simple implementation that only supports one icon set.
- // Fetch the set and extract the icon.
- const firstConfig = unfetchedIconSetConfigs[0];
- return this._loadIconSetFromConfig(firstConfig)
- .do((iconSet: SVGElement) => firstConfig.svgElement = iconSet)
- .map((iconSet: SVGElement) => {
- const namedIcon = this._extractSvgIconFromSet(iconSet, iconName, firstConfig);
- if (!namedIcon) {
- throw Error(`Failed to find icon name: ${iconName} in set: ${setName}`);
- }
- return namedIcon;
- });
- */
}
}
- return Observable.throw(Error(`Unknown icon name: ${iconName} in set: ${setName}`));
+ return null;
}
loadIconFromUrl(url: string): Observable {
From 17f40c5b49fb0c223fb196020cfececb37212c35 Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Fri, 8 Apr 2016 14:52:39 -0400
Subject: [PATCH 16/37] fix tests
---
src/components/icon/icon.spec.ts | 18 +++++++-----------
1 file changed, 7 insertions(+), 11 deletions(-)
diff --git a/src/components/icon/icon.spec.ts b/src/components/icon/icon.spec.ts
index 836e8f913414..1d5116936d26 100644
--- a/src/components/icon/icon.spec.ts
+++ b/src/components/icon/icon.spec.ts
@@ -1,8 +1,14 @@
import {
+ it, iit,
+ describe,
+ ddescribe,
+ expect,
+ beforeEach,
+ beforeEachProviders,
+ xit,
inject,
TestComponentBuilder,
} from 'angular2/testing';
-
import {
HTTP_PROVIDERS,
BaseRequestOptions,
@@ -13,15 +19,6 @@ import {
import {
MockBackend,
MockConnection} from 'angular2/http/testing';
-import {
- it, iit,
- describe,
- ddescribe,
- expect,
- beforeEach,
- beforeEachProviders,
- xit,
-} from '../../core/facade/testing';
import {
provide,
Component} from 'angular2/core';
@@ -300,7 +297,6 @@ class MdIconCustomFontCssTestApp {
fontIcon = '';
}
-
@Component({
selector: 'test-app',
template: ``,
From 87dd4acd2b121c9128fd89a393a229cbb437b61f Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Fri, 8 Apr 2016 14:57:45 -0400
Subject: [PATCH 17/37] Remove local changes.
---
src/demo-app/demo-app.scss | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/demo-app/demo-app.scss b/src/demo-app/demo-app.scss
index 07d3aee6e908..f23099e1cde8 100644
--- a/src/demo-app/demo-app.scss
+++ b/src/demo-app/demo-app.scss
@@ -10,6 +10,6 @@
}
}
-.md-icon-content {
- opacity: 0.5;
+.demo-content {
+ padding: 15px;
}
From 82bd25ef3e5b20671325673668cb526cbe86584b Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Fri, 8 Apr 2016 15:00:07 -0400
Subject: [PATCH 18/37] remove unused entry
---
test/karma.config.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/test/karma.config.ts b/test/karma.config.ts
index f57b5b9b3505..c41c3d0183bf 100644
--- a/test/karma.config.ts
+++ b/test/karma.config.ts
@@ -32,7 +32,6 @@ export function config(config) {
{pattern: 'node_modules/angular2/bundles/angular2.dev.js', included: true, watched: true},
{pattern: 'node_modules/angular2/bundles/testing.dev.js', included: true, watched: true},
{pattern: 'node_modules/angular2/bundles/http.dev.js', included: true, watched: true},
- //{pattern: 'node_modules/angular2/http/testing.js', included: true, watched: true},
{pattern: 'test/karma-test-shim.js', included: true, watched: true},
From e4d568f6d7fd15c997317524b284a81df4b183f8 Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Sun, 10 Apr 2016 02:04:13 -0400
Subject: [PATCH 19/37] more tests
---
src/components/icon/icon-provider.ts | 53 +++---
src/components/icon/icon.spec.ts | 234 ++++++++++++++++++++++-----
src/components/icon/icon.ts | 27 ++--
src/demo-app/icon/icon-demo.html | 2 +
4 files changed, 237 insertions(+), 79 deletions(-)
diff --git a/src/components/icon/icon-provider.ts b/src/components/icon/icon-provider.ts
index 361a807f6c36..baa55e41dc84 100644
--- a/src/components/icon/icon-provider.ts
+++ b/src/components/icon/icon-provider.ts
@@ -20,7 +20,7 @@ export class MdIconProvider {
// IconConfig objects and cached SVG elements for icon sets.
// These are stored only by set name, but multiple URLs can be registered under the same name.
- private _iconSetConfigs = new Map();
+ private _iconSetConfigs = new Map();
// Cache for icons loaded by direct URLs.
private _cachedIconsByUrl = new Map();
@@ -28,19 +28,19 @@ export class MdIconProvider {
private _fontClassNamesByAlias = new Map();
private _inProgressUrlFetches = new Map>();
-
+
private _defaultViewBoxSize = 24;
private _defaultFontSetClass = 'material-icons';
-
+
constructor(private _http: Http) {
}
-
- public addIcon(iconName: string, url: string, viewBoxSize:number=0): MdIconProvider {
+
+ public addIcon(iconName: string, url: string, viewBoxSize:number=0): this {
return this.addIconInSet('', iconName, url, viewBoxSize);
}
-
+
public addIconInSet(
- setName: string, iconName: string, url: string, viewBoxSize:number=0): MdIconProvider {
+ setName: string, iconName: string, url: string, viewBoxSize:number=0): this {
let iconSetMap = this._iconConfigs.get(setName);
if (!iconSetMap) {
iconSetMap = new Map();
@@ -49,8 +49,8 @@ export class MdIconProvider {
iconSetMap.set(iconName, new IconConfig(url, viewBoxSize || this._defaultViewBoxSize));
return this;
}
-
- public addIconSet(setName: string, url: string, viewBoxSize=0): MdIconProvider {
+
+ public addIconSet(setName: string, url: string, viewBoxSize=0): this {
const config = new IconConfig(url, viewBoxSize || this._defaultViewBoxSize);
if (this._iconSetConfigs.has(setName)) {
this._iconSetConfigs.get(setName).push(config);
@@ -59,26 +59,26 @@ export class MdIconProvider {
}
return this;
}
-
- public registerFontSet(alias: string, className?: string): MdIconProvider {
+
+ public registerFontSet(alias: string, className?: string): this {
this._fontClassNamesByAlias.set(alias, className || alias);
return this;
}
-
+
public setDefaultViewBoxSize(size: number) {
this._defaultViewBoxSize = size;
return this;
}
-
+
public getDefaultViewBoxSize(): number {
return this._defaultViewBoxSize;
}
-
+
public setDefaultFontSetClass(className: string) {
this._defaultFontSetClass = className;
return this;
}
-
+
public getDefaultFontSetClass(): string {
return this._defaultFontSetClass;
}
@@ -145,7 +145,7 @@ export class MdIconProvider {
return Observable.throw(Error(`Unknown icon name: ${iconName} in set: ${setName}`));
}
- private _extractIconWithNameFromAnySet(iconName: string, setConfigs: [IconConfig]): SVGElement {
+ private _extractIconWithNameFromAnySet(iconName: string, setConfigs: IconConfig[]): SVGElement {
// Iterate backwards, so icon sets added later have precedence.
for (let i = setConfigs.length - 1; i >= 0; i--) {
const config = setConfigs[i];
@@ -158,7 +158,7 @@ export class MdIconProvider {
}
return null;
}
-
+
loadIconFromUrl(url: string): Observable {
if (this._cachedIconsByUrl.has(url)) {
return Observable.of(this._cachedIconsByUrl.get(url).cloneNode(true));
@@ -168,16 +168,15 @@ export class MdIconProvider {
}
classNameForFontAlias(alias: string): string {
- if (!this._fontClassNamesByAlias.has(alias)) {
- throw Error('Unknown font alias: ' + alias);
- }
- return this._fontClassNamesByAlias.get(alias);
+ return this._fontClassNamesByAlias.get(alias) || alias;
}
-
+
private _fetchUrl(url: string): Observable {
// FIXME: This is trying to avoid sending a duplicate request for a URL when there is already
// a request in progress for that URL. But it's not working; even though we return the cached
// Observable, a second request is still sent.
+ // Observable.share seems like it should work, but doesn't seem to have any effect.
+ // (http://xgrommx.github.io/rx-book/content/observable/observable_instance_methods/share.html)
console.log('*** fetchUrl: ' + url);
if (this._inProgressUrlFetches.has(url)) {
console.log("*** Using existing request");
@@ -192,19 +191,19 @@ export class MdIconProvider {
})
.map((response) => response.text());
this._inProgressUrlFetches.set(url, req);
- return req;
+ return req.share();
}
-
+
private _loadIconFromConfig(config: IconConfig): Observable {
return this._fetchUrl(config.url)
.map(svgText => this._createSvgElementForSingleIcon(svgText, config));
}
-
+
private _loadIconSetFromConfig(config: IconConfig): Observable {
return this._fetchUrl(config.url)
.map((svgText) => this._svgElementFromString(svgText));
}
-
+
private _createSvgElementForSingleIcon(responseText: string, config: IconConfig): SVGElement {
const svg = this._svgElementFromString(responseText);
this._setSvgAttributes(svg, config);
@@ -224,7 +223,7 @@ export class MdIconProvider {
svg.getAttribute('viewBox') || ('0 0 ' + viewBoxSize + ' ' + viewBoxSize));
svg.setAttribute('focusable', 'false'); // Disable IE11 default behavior to make SVGs focusable.
}
-
+
private _extractSvgIconFromSet(
iconSet: SVGElement, iconName: string, config: IconConfig): SVGElement {
const iconNode = iconSet.querySelector('#' + iconName);
diff --git a/src/components/icon/icon.spec.ts b/src/components/icon/icon.spec.ts
index 1d5116936d26..2df8e6febe90 100644
--- a/src/components/icon/icon.spec.ts
+++ b/src/components/icon/icon.spec.ts
@@ -1,11 +1,9 @@
import {
- it, iit,
+ it,
describe,
- ddescribe,
expect,
beforeEach,
beforeEachProviders,
- xit,
inject,
TestComponentBuilder,
} from 'angular2/testing';
@@ -35,16 +33,17 @@ const sortedClassNames = (elem: Element) => elem.className.split(' ').sort();
export function main() {
describe('MdIcon', () => {
-
+
beforeEachProviders(() => [
MdIconProvider,
HTTP_PROVIDERS,
MockBackend,
provide(XHRBackend, {useExisting: MockBackend}),
]);
-
+
let builder: TestComponentBuilder;
let mdIconProvider: MdIconProvider;
+ let httpRequestUrls: string[];
beforeEach(
inject([TestComponentBuilder, MdIconProvider, MockBackend],
@@ -53,8 +52,11 @@ export function main() {
mdIconProvider = mip;
// Set fake responses for various SVG URLs.
// TODO: Keep track of requests so we can verify caching behavior.
+ httpRequestUrls = [];
mockBackend.connections.subscribe((connection: any) => {
- switch (connection.request.url) {
+ const url = connection.request.url;
+ httpRequestUrls.push(url);
+ switch (url) {
case 'cat.svg':
connection.mockRespond(new Response(new ResponseOptions({
status: 200,
@@ -67,7 +69,7 @@ export function main() {
body: '',
})));
break;
- case 'farm-set.svg':
+ case 'farm-set-1.svg':
connection.mockRespond(new Response(new ResponseOptions({
status: 200,
body: `
@@ -78,6 +80,28 @@ export function main() {
`,
})));
break;
+ case 'farm-set-2.svg':
+ connection.mockRespond(new Response(new ResponseOptions({
+ status: 200,
+ body: `
+
+
+
+
+ `,
+ })));
+ break;
+ case 'arrow-set.svg':
+ connection.mockRespond(new Response(new ResponseOptions({
+ status: 200,
+ body: `
+
+
+
+
+ `,
+ })));
+ break;
}
});
}));
@@ -94,30 +118,14 @@ export function main() {
});
});
- it('should set aria label from text content if not specified', (done: () => void) => {
+ it('should use alternate icon font if set', (done: () => void) => {
+ mdIconProvider.setDefaultFontSetClass('myfont');
return builder.createAsync(MdIconLigatureTestApp).then((fixture) => {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
testComponent.iconName = 'home';
fixture.detectChanges();
- expect(mdIconElement.getAttribute('aria-label')).toBe('home');
-
- testComponent.iconName = 'hand';
- fixture.detectChanges();
- expect(mdIconElement.getAttribute('aria-label')).toBe('hand');
-
- done();
- });
- });
-
- it('should use provided aria label', (done: () => void) => {
- return builder.createAsync(MdIconLigatureWithAriaBindingTestApp).then((fixture) => {
- const testComponent = fixture.debugElement.componentInstance;
- const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
- testComponent.iconName = 'home';
- testComponent.ariaLabel = 'house';
- fixture.detectChanges();
- expect(mdIconElement.getAttribute('aria-label')).toBe('house');
+ expect(sortedClassNames(mdIconElement)).toEqual(['myfont']);
done();
});
});
@@ -128,12 +136,14 @@ export function main() {
return builder.createAsync(MdIconFromSvgUrlTestApp).then((fixture) => {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
+ let svgElement: any;
+ let pathElement: any;
+
testComponent.iconUrl = 'cat.svg';
fixture.detectChanges();
-
// An element should have been added as a child of .
expect(mdIconElement.children.length).toBe(1);
- let svgElement = mdIconElement.children[0];
+ svgElement = mdIconElement.children[0];
expect(svgElement.tagName.toLowerCase()).toBe('svg');
// Default attributes should be set.
expect(svgElement.getAttribute('height')).toBe('100%');
@@ -141,7 +151,7 @@ export function main() {
expect(svgElement.getAttribute('viewBox')).toBe('0 0 24 24');
// Make sure SVG content is taken from response.
expect(svgElement.children.length).toBe(1);
- let pathElement = svgElement.children[0];
+ pathElement = svgElement.children[0];
expect(pathElement.tagName.toLowerCase()).toBe('path');
expect(pathElement.getAttribute('d')).toBe('meow');
@@ -156,6 +166,15 @@ export function main() {
expect(pathElement.tagName.toLowerCase()).toBe('path');
expect(pathElement.getAttribute('d')).toBe('woof');
+ expect(httpRequestUrls).toEqual(['cat.svg', 'dog.svg']);
+ // Using an icon from a previously loaded URL should not cause another HTTP request.
+ testComponent.iconUrl = 'cat.svg';
+ fixture.detectChanges();
+ expect(mdIconElement.children.length).toBe(1);
+ pathElement = mdIconElement.querySelector('svg path');
+ expect(pathElement.getAttribute('d')).toBe('meow');
+ expect(httpRequestUrls).toEqual(['cat.svg', 'dog.svg']);
+
done();
});
});
@@ -166,13 +185,16 @@ export function main() {
return builder.createAsync(MdIconFromSvgNameTestApp).then((fixture) => {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
+ let svgElement: any;
+ let pathElement: any;
+
testComponent.iconName= 'fido';
fixture.detectChanges();
expect(mdIconElement.children.length).toBe(1);
- let svgElement = mdIconElement.children[0];
+ svgElement = mdIconElement.children[0];
expect(svgElement.tagName.toLowerCase()).toBe('svg');
expect(svgElement.children.length).toBe(1);
- let pathElement = svgElement.children[0];
+ pathElement = svgElement.children[0];
expect(pathElement.tagName.toLowerCase()).toBe('path');
expect(pathElement.getAttribute('d')).toBe('woof');
// The aria label should be taken from the icon name.
@@ -191,27 +213,40 @@ export function main() {
expect(pathElement.getAttribute('d')).toBe('meow');
expect(mdIconElement.getAttribute('aria-label')).toBe('fluffy');
+ expect(httpRequestUrls).toEqual(['dog.svg', 'cat.svg']);
+ // Using an icon from a previously loaded URL should not cause another HTTP request.
+ testComponent.iconName = 'fido';
+ fixture.detectChanges();
+ expect(mdIconElement.children.length).toBe(1);
+ pathElement = mdIconElement.querySelector('svg path');
+ expect(pathElement.getAttribute('d')).toBe('woof');
+ expect(httpRequestUrls).toEqual(['dog.svg', 'cat.svg']);
+
done();
});
});
it('should extract icon from SVG icon set', (done: () => void) => {
- mdIconProvider.addIconSet('farm', 'farm-set.svg');
+ mdIconProvider.addIconSet('farm', 'farm-set-1.svg');
return builder.createAsync(MdIconFromSvgNameTestApp).then((fixture) => {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
+ let svgElement: any;
+ let pathElement: any;
+ let svgChild: any;
+
testComponent.iconName = 'farm:pig';
fixture.detectChanges();
expect(mdIconElement.children.length).toBe(1);
- let svgElement = mdIconElement.children[0];
+ svgElement = mdIconElement.children[0];
expect(svgElement.tagName.toLowerCase()).toBe('svg');
expect(svgElement.children.length).toBe(1);
- let svgChild = svgElement.children[0];
+ svgChild = svgElement.children[0];
// The first child should be the element.
expect(svgChild.tagName.toLowerCase()).toBe('g');
expect(svgChild.getAttribute('id')).toBe('pig');
expect(svgChild.children.length).toBe(1);
- let pathElement = svgChild.children[0];
+ pathElement = svgChild.children[0];
expect(pathElement.tagName.toLowerCase()).toBe('path');
expect(pathElement.getAttribute('d')).toBe('oink');
// The aria label should be taken from the icon name (without the icon set portion).
@@ -223,7 +258,7 @@ export function main() {
expect(mdIconElement.children.length).toBe(1);
svgElement = mdIconElement.children[0];
svgChild = svgElement.children[0];
- // The first child should be the element.
+ // The first child should be the element.
expect(svgChild.tagName.toLowerCase()).toBe('g');
expect(svgChild.getAttribute('id')).toBe('cow');
expect(svgChild.children.length).toBe(1);
@@ -235,8 +270,61 @@ export function main() {
done();
});
});
+
+ it('should allow multiple icon sets in a namespace', (done: () => void) => {
+ mdIconProvider.addIconSet('farm', 'farm-set-1.svg');
+ mdIconProvider.addIconSet('farm', 'farm-set-2.svg');
+ mdIconProvider.addIconSet('arrows', 'arrow-set.svg');
+ return builder.createAsync(MdIconFromSvgNameTestApp).then((fixture) => {
+ const testComponent = fixture.debugElement.componentInstance;
+ const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
+ let svgElement: any;
+ let pathElement: any;
+ let svgChild: any;
+
+ testComponent.iconName = 'farm:pig';
+ fixture.detectChanges();
+ expect(mdIconElement.children.length).toBe(1);
+ svgElement = mdIconElement.children[0];
+ expect(svgElement.tagName.toLowerCase()).toBe('svg');
+ expect(svgElement.children.length).toBe(1);
+ svgChild = svgElement.children[0];
+ // The first child should be the element.
+ expect(svgChild.tagName.toLowerCase()).toBe('g');
+ expect(svgChild.getAttribute('id')).toBe('pig');
+ expect(svgChild.children.length).toBe(1);
+ pathElement = svgChild.children[0];
+ expect(pathElement.tagName.toLowerCase()).toBe('path');
+ expect(pathElement.getAttribute('d')).toBe('oink');
+ // The aria label should be taken from the icon name (without the icon set portion).
+ expect(mdIconElement.getAttribute('aria-label')).toBe('pig');
+
+ // Both icon sets registered in the 'farm' namespace should have been fetched.
+ expect(httpRequestUrls.sort()).toEqual(['farm-set-1.svg', 'farm-set-2.svg']);
+
+ // Change the icon name to one that appears in both icon sets. The icon from the set that
+ // was registered last should be used (with 'd' attribute of 'moo moo' instead of 'moo'),
+ // and no additional HTTP request should be made.
+ testComponent.iconName = 'farm:cow';
+ fixture.detectChanges();
+ expect(mdIconElement.children.length).toBe(1);
+ svgElement = mdIconElement.children[0];
+ svgChild = svgElement.children[0];
+ // The first child should be the element.
+ expect(svgChild.tagName.toLowerCase()).toBe('g');
+ expect(svgChild.getAttribute('id')).toBe('cow');
+ expect(svgChild.children.length).toBe(1);
+ pathElement = svgChild.children[0];
+ expect(pathElement.tagName.toLowerCase()).toBe('path');
+ expect(pathElement.getAttribute('d')).toBe('moo moo');
+ expect(mdIconElement.getAttribute('aria-label')).toBe('cow');
+ expect(httpRequestUrls.sort()).toEqual(['farm-set-1.svg', 'farm-set-2.svg']);
+
+ done();
+ });
+ });
});
-
+
describe('custom fonts', () => {
it('should apply CSS classes for custom font and icon', (done: () => void) => {
mdIconProvider.registerFontSet('f1', 'font1');
@@ -249,13 +337,79 @@ export function main() {
fixture.detectChanges();
expect(sortedClassNames(mdIconElement)).toEqual(['font1', 'house']);
expect(mdIconElement.getAttribute('aria-label')).toBe('house');
-
+
testComponent.fontSet = 'f2';
testComponent.fontIcon = 'igloo';
fixture.detectChanges();
expect(sortedClassNames(mdIconElement)).toEqual(['f2', 'igloo']);
expect(mdIconElement.getAttribute('aria-label')).toBe('igloo');
-
+
+ testComponent.fontSet = 'f3';
+ testComponent.fontIcon = 'tent';
+ fixture.detectChanges();
+ expect(sortedClassNames(mdIconElement)).toEqual(['f3', 'tent']);
+ expect(mdIconElement.getAttribute('aria-label')).toBe('tent');
+
+ done();
+ });
+ });
+ });
+
+ describe('aria label', () => {
+ it('should set aria label from text content if not specified', (done: () => void) => {
+ return builder.createAsync(MdIconLigatureTestApp).then((fixture) => {
+ const testComponent = fixture.debugElement.componentInstance;
+ const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
+ testComponent.iconName = 'home';
+ fixture.detectChanges();
+ expect(mdIconElement.getAttribute('aria-label')).toBe('home');
+
+ testComponent.iconName = 'hand';
+ fixture.detectChanges();
+ expect(mdIconElement.getAttribute('aria-label')).toBe('hand');
+
+ done();
+ });
+ });
+
+ it('should use alt tag if aria label is not specified', (done: () => void) => {
+ return builder.createAsync(MdIconLigatureWithAriaBindingTestApp).then((fixture) => {
+ const testComponent = fixture.debugElement.componentInstance;
+ const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
+ testComponent.iconName = 'home';
+ testComponent.altText = 'castle';
+ fixture.detectChanges();
+ expect(mdIconElement.getAttribute('aria-label')).toBe('castle');
+
+ testComponent.ariaLabel = 'house';
+ fixture.detectChanges();
+ expect(mdIconElement.getAttribute('aria-label')).toBe('house');
+
+ done();
+ });
+ });
+
+ it('should use provided aria label rather than icon name', (done: () => void) => {
+ return builder.createAsync(MdIconLigatureWithAriaBindingTestApp).then((fixture) => {
+ const testComponent = fixture.debugElement.componentInstance;
+ const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
+ testComponent.iconName = 'home';
+ testComponent.ariaLabel = 'house';
+ fixture.detectChanges();
+ expect(mdIconElement.getAttribute('aria-label')).toBe('house');
+ done();
+ });
+ });
+
+ it('should use provided aria label rather than font icon', (done: () => void) => {
+ return builder.createAsync(MdIconCustomFontCssTestApp).then((fixture) => {
+ const testComponent = fixture.debugElement.componentInstance;
+ const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
+ testComponent.fontSet = 'f1';
+ testComponent.fontIcon = 'house';
+ testComponent.ariaLabel = 'home';
+ fixture.detectChanges();
+ expect(mdIconElement.getAttribute('aria-label')).toBe('home');
done();
});
});
@@ -276,7 +430,7 @@ class MdIconLigatureTestApp {
@Component({
selector: 'test-app',
- template: `{{iconName}}`,
+ template: `{{iconName}}`,
directives: [MdIcon],
})
class MdIconLigatureWithAriaBindingTestApp {
diff --git a/src/components/icon/icon.ts b/src/components/icon/icon.ts
index eb41cb4f9fe9..fa06b51c26a2 100644
--- a/src/components/icon/icon.ts
+++ b/src/components/icon/icon.ts
@@ -70,19 +70,22 @@ export class MdIcon implements OnChanges, OnInit, AfterContentChecked {
return [iconName.substring(0, sepIndex), iconName.substring(sepIndex + 1)];
}
- ngOnChanges(changes: {[propertyName: string]: SimpleChange}) {
- // TODO: Only refetch icons if the values actually changed.
- if (this.svgIcon) {
- const [iconSet, iconName] = this._splitIconName(this.svgIcon);
- this._mdIconProvider.loadIconFromSetByName(iconSet, iconName)
+ ngOnChanges(changes: { [propertyName: string]: SimpleChange }) {
+ const changedInputs = Object.keys(changes);
+ // Only update the inline SVG icon if the inputs changed, to avoid unnecessary DOM operations.
+ if (changedInputs.indexOf('svgIcon') != -1 || changedInputs.indexOf('svgSrc') != -1) {
+ if (this.svgIcon) {
+ const [iconSet, iconName] = this._splitIconName(this.svgIcon);
+ this._mdIconProvider.loadIconFromSetByName(iconSet, iconName)
.subscribe(
- (svg: SVGElement) => this._setSvgElement(svg),
- (err: any) => console.log(`Error retrieving icon: ${err}`));
- } else if (this.svgSrc) {
- this._mdIconProvider.loadIconFromUrl(this.svgSrc)
- .subscribe(
- (svg: SVGElement) => this._setSvgElement(svg),
- (err: any) => console.log(`Error retrieving icon: ${err}`));
+ (svg: SVGElement) => this._setSvgElement(svg),
+ (err: any) => console.log(`Error retrieving icon: ${err}`));
+ } else if (this.svgSrc) {
+ this._mdIconProvider.loadIconFromUrl(this.svgSrc)
+ .subscribe(
+ (svg: SVGElement) => this._setSvgElement(svg),
+ (err: any) => console.log(`Error retrieving icon: ${err}`));
+ }
}
if (this._usingFontIcon()) {
this._updateFontIconClasses();
diff --git a/src/demo-app/icon/icon-demo.html b/src/demo-app/icon/icon-demo.html
index b3daf73d3471..0de32df13b23 100644
--- a/src/demo-app/icon/icon-demo.html
+++ b/src/demo-app/icon/icon-demo.html
@@ -10,11 +10,13 @@
By name registered with MdIconProvider:
+
From icon set:
+
From af2f94b0c4125dd4cc4c4a46e5f101cb0be788d0 Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Sun, 10 Apr 2016 14:19:46 -0400
Subject: [PATCH 20/37] MdIconProvider->MdIconRegistry
---
.../{icon-provider.ts => icon-registry.ts} | 30 ++++++++---------
src/components/icon/icon.spec.ts | 32 +++++++++----------
src/components/icon/icon.ts | 26 +++++++--------
src/demo-app/icon/icon-demo.ts | 8 ++---
src/main.ts | 4 +--
5 files changed, 50 insertions(+), 50 deletions(-)
rename src/components/icon/{icon-provider.ts => icon-registry.ts} (91%)
diff --git a/src/components/icon/icon-provider.ts b/src/components/icon/icon-registry.ts
similarity index 91%
rename from src/components/icon/icon-provider.ts
rename to src/components/icon/icon-registry.ts
index baa55e41dc84..4e8010f85159 100644
--- a/src/components/icon/icon-provider.ts
+++ b/src/components/icon/icon-registry.ts
@@ -12,14 +12,14 @@ class IconConfig {
}
@Injectable()
-export class MdIconProvider {
+export class MdIconRegistry {
// IconConfig objects and cached SVG elements for individual icons.
- // First level of cache is icon set (which is the empty string for the default set).
- // Second level is the icon name within the set.
+ // First level of cache is namespace (which is the empty string if not specified).
+ // Second level is the icon name within the namespace.
private _iconConfigs = new Map>();
// IconConfig objects and cached SVG elements for icon sets.
- // These are stored only by set name, but multiple URLs can be registered under the same name.
+ // Multiple icon sets can be registered under the same namespace.
private _iconSetConfigs = new Map();
// Cache for icons loaded by direct URLs.
@@ -36,15 +36,15 @@ export class MdIconProvider {
}
public addIcon(iconName: string, url: string, viewBoxSize:number=0): this {
- return this.addIconInSet('', iconName, url, viewBoxSize);
+ return this.addIconInNamespace('', iconName, url, viewBoxSize);
}
- public addIconInSet(
- setName: string, iconName: string, url: string, viewBoxSize:number=0): this {
- let iconSetMap = this._iconConfigs.get(setName);
+ public addIconInNamespace(
+ namespace: string, iconName: string, url: string, viewBoxSize:number=0): this {
+ let iconSetMap = this._iconConfigs.get(namespace);
if (!iconSetMap) {
iconSetMap = new Map();
- this._iconConfigs.set(setName, iconSetMap);
+ this._iconConfigs.set(namespace, iconSetMap);
}
iconSetMap.set(iconName, new IconConfig(url, viewBoxSize || this._defaultViewBoxSize));
return this;
@@ -83,10 +83,10 @@ export class MdIconProvider {
return this._defaultFontSetClass;
}
- loadIconFromSetByName(setName: string, iconName: string): Observable {
+ loadIconFromNamespaceByName(namespace: string, iconName: string): Observable {
// Return (copy of) cached icon if possible.
- if (this._iconConfigs.has(setName) && this._iconConfigs.get(setName).has(iconName)) {
- const config = this._iconConfigs.get(setName).get(iconName);
+ if (this._iconConfigs.has(namespace) && this._iconConfigs.get(namespace).has(iconName)) {
+ const config = this._iconConfigs.get(namespace).get(iconName);
if (config.svgElement) {
// We already have the SVG element for this icon, return a copy.
return Observable.of(config.svgElement.cloneNode(true));
@@ -98,7 +98,7 @@ export class MdIconProvider {
}
}
// See if we have any icon sets registered for the set name.
- const iconSetConfigs = this._iconSetConfigs.get(setName);
+ const iconSetConfigs = this._iconSetConfigs.get(namespace);
if (iconSetConfigs) {
// For all the icon set SVG elements we've fetched, see if any contain an icon with the
// requested name.
@@ -137,12 +137,12 @@ export class MdIconProvider {
.map((ignoredResults: any) => {
const foundIcon = this._extractIconWithNameFromAnySet(iconName, iconSetConfigs);
if (!foundIcon) {
- throw Error(`Failed to find icon name: ${iconName} in set: ${setName}`);
+ throw Error(`Failed to find icon name: ${iconName} in namespace: ${namespace}`);
}
return foundIcon;
});
}
- return Observable.throw(Error(`Unknown icon name: ${iconName} in set: ${setName}`));
+ return Observable.throw(Error(`Unknown icon name: ${iconName} in namespace: ${namespace}`));
}
private _extractIconWithNameFromAnySet(iconName: string, setConfigs: IconConfig[]): SVGElement {
diff --git a/src/components/icon/icon.spec.ts b/src/components/icon/icon.spec.ts
index 2df8e6febe90..f9d2038d956b 100644
--- a/src/components/icon/icon.spec.ts
+++ b/src/components/icon/icon.spec.ts
@@ -27,7 +27,7 @@ import {
} from 'angular2/platform/testing/browser';
import {MdIcon} from './icon';
-import {MdIconProvider} from './icon-provider';
+import {MdIconRegistry} from './icon-registry';
const sortedClassNames = (elem: Element) => elem.className.split(' ').sort();
@@ -35,23 +35,23 @@ export function main() {
describe('MdIcon', () => {
beforeEachProviders(() => [
- MdIconProvider,
+ MdIconRegistry,
HTTP_PROVIDERS,
MockBackend,
provide(XHRBackend, {useExisting: MockBackend}),
]);
let builder: TestComponentBuilder;
- let mdIconProvider: MdIconProvider;
+ let mdIconRegistry: MdIconRegistry;
let httpRequestUrls: string[];
beforeEach(
- inject([TestComponentBuilder, MdIconProvider, MockBackend],
- (tcb: TestComponentBuilder, mip: MdIconProvider, mockBackend: MockBackend) => {
+ inject([TestComponentBuilder, MdIconRegistry, MockBackend],
+ (tcb: TestComponentBuilder, mir: MdIconRegistry, mockBackend: MockBackend) => {
builder = tcb;
- mdIconProvider = mip;
+ mdIconRegistry = mir;
// Set fake responses for various SVG URLs.
- // TODO: Keep track of requests so we can verify caching behavior.
+ // Keep track of requests so we can verify caching behavior.
httpRequestUrls = [];
mockBackend.connections.subscribe((connection: any) => {
const url = connection.request.url;
@@ -119,7 +119,7 @@ export function main() {
});
it('should use alternate icon font if set', (done: () => void) => {
- mdIconProvider.setDefaultFontSetClass('myfont');
+ mdIconRegistry.setDefaultFontSetClass('myfont');
return builder.createAsync(MdIconLigatureTestApp).then((fixture) => {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
@@ -180,8 +180,8 @@ export function main() {
});
it('should register icon URLs by name', (done: () => void) => {
- mdIconProvider.addIcon('fluffy', 'cat.svg');
- mdIconProvider.addIcon('fido', 'dog.svg');
+ mdIconRegistry.addIcon('fluffy', 'cat.svg');
+ mdIconRegistry.addIcon('fido', 'dog.svg');
return builder.createAsync(MdIconFromSvgNameTestApp).then((fixture) => {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
@@ -227,7 +227,7 @@ export function main() {
});
it('should extract icon from SVG icon set', (done: () => void) => {
- mdIconProvider.addIconSet('farm', 'farm-set-1.svg');
+ mdIconRegistry.addIconSet('farm', 'farm-set-1.svg');
return builder.createAsync(MdIconFromSvgNameTestApp).then((fixture) => {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
@@ -272,9 +272,9 @@ export function main() {
});
it('should allow multiple icon sets in a namespace', (done: () => void) => {
- mdIconProvider.addIconSet('farm', 'farm-set-1.svg');
- mdIconProvider.addIconSet('farm', 'farm-set-2.svg');
- mdIconProvider.addIconSet('arrows', 'arrow-set.svg');
+ mdIconRegistry.addIconSet('farm', 'farm-set-1.svg');
+ mdIconRegistry.addIconSet('farm', 'farm-set-2.svg');
+ mdIconRegistry.addIconSet('arrows', 'arrow-set.svg');
return builder.createAsync(MdIconFromSvgNameTestApp).then((fixture) => {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
@@ -327,8 +327,8 @@ export function main() {
describe('custom fonts', () => {
it('should apply CSS classes for custom font and icon', (done: () => void) => {
- mdIconProvider.registerFontSet('f1', 'font1');
- mdIconProvider.registerFontSet('f2');
+ mdIconRegistry.registerFontSet('f1', 'font1');
+ mdIconRegistry.registerFontSet('f2');
return builder.createAsync(MdIconCustomFontCssTestApp).then((fixture) => {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
diff --git a/src/components/icon/icon.ts b/src/components/icon/icon.ts
index fa06b51c26a2..cc31d302e2fa 100644
--- a/src/components/icon/icon.ts
+++ b/src/components/icon/icon.ts
@@ -16,8 +16,8 @@ import {
import {NgClass} from 'angular2/common';
import {
- MdIconProvider,
-} from './icon-provider';
+ MdIconRegistry,
+} from './icon-registry';
@Component({
template: '',
@@ -35,17 +35,17 @@ export class MdIcon implements OnChanges, OnInit, AfterContentChecked {
@Input() fontSet: string;
@Input() fontIcon: string;
@Input() alt: string;
-
+
@Input('aria-label') ariaLabelFromParent: string = '';
-
+
private _previousFontSetClass: string;
private _previousFontIconClass: string;
-
+
constructor(
private _element: ElementRef,
private _renderer: Renderer,
private _changeDetectorRef: ChangeDetectorRef,
- private _mdIconProvider: MdIconProvider) {
+ private _mdIconRegistry: MdIconRegistry) {
}
/**
@@ -75,13 +75,13 @@ export class MdIcon implements OnChanges, OnInit, AfterContentChecked {
// Only update the inline SVG icon if the inputs changed, to avoid unnecessary DOM operations.
if (changedInputs.indexOf('svgIcon') != -1 || changedInputs.indexOf('svgSrc') != -1) {
if (this.svgIcon) {
- const [iconSet, iconName] = this._splitIconName(this.svgIcon);
- this._mdIconProvider.loadIconFromSetByName(iconSet, iconName)
+ const [namespace, iconName] = this._splitIconName(this.svgIcon);
+ this._mdIconRegistry.loadIconFromNamespaceByName(namespace, iconName)
.subscribe(
(svg: SVGElement) => this._setSvgElement(svg),
(err: any) => console.log(`Error retrieving icon: ${err}`));
} else if (this.svgSrc) {
- this._mdIconProvider.loadIconFromUrl(this.svgSrc)
+ this._mdIconRegistry.loadIconFromUrl(this.svgSrc)
.subscribe(
(svg: SVGElement) => this._setSvgElement(svg),
(err: any) => console.log(`Error retrieving icon: ${err}`));
@@ -114,7 +114,7 @@ export class MdIcon implements OnChanges, OnInit, AfterContentChecked {
this._changeDetectorRef.detectChanges();
}
}
-
+
private _getAriaLabel() {
// If the parent provided an aria-label attribute value, use it as-is. Otherwise look for a
// reasonable value from the alt attribute, font icon name, SVG icon name, or (for ligatures)
@@ -141,7 +141,7 @@ export class MdIcon implements OnChanges, OnInit, AfterContentChecked {
private _usingFontIcon(): boolean {
return !(this.svgIcon || this.svgSrc);
}
-
+
private _setSvgElement(svg: SVGElement) {
// Can we use Renderer here somehow?
const layoutElement = this._element.nativeElement;
@@ -155,8 +155,8 @@ export class MdIcon implements OnChanges, OnInit, AfterContentChecked {
}
const elem = this._element.nativeElement;
const fontSetClass = this.fontSet ?
- this._mdIconProvider.classNameForFontAlias(this.fontSet) :
- this._mdIconProvider.getDefaultFontSetClass();
+ this._mdIconRegistry.classNameForFontAlias(this.fontSet) :
+ this._mdIconRegistry.getDefaultFontSetClass();
if (fontSetClass != this._previousFontSetClass) {
if (this._previousFontSetClass) {
this._renderer.setElementClass(elem, this._previousFontSetClass, false);
diff --git a/src/demo-app/icon/icon-demo.ts b/src/demo-app/icon/icon-demo.ts
index a262bb901e8e..a5d8fe51a68e 100644
--- a/src/demo-app/icon/icon-demo.ts
+++ b/src/demo-app/icon/icon-demo.ts
@@ -1,20 +1,20 @@
import {Component, ViewEncapsulation} from 'angular2/core';
import {MdIcon} from '../../components/icon/icon';
-import {MdIconProvider} from '../../components/icon/icon-provider';
+import {MdIconRegistry} from '../../components/icon/icon-registry';
@Component({
selector: 'icon-demo',
templateUrl: 'demo-app/icon/icon-demo.html',
styleUrls: ['demo-app/icon/icon-demo.css'],
directives: [MdIcon],
- viewProviders: [MdIconProvider],
+ viewProviders: [MdIconRegistry],
encapsulation: ViewEncapsulation.None,
})
export class IconDemo {
showAndroid = true;
- constructor(mdIconProvider: MdIconProvider) {
- mdIconProvider
+ constructor(mdIconRegistry: MdIconRegistry) {
+ mdIconRegistry
.addIcon('thumb-up', '/demo-app/icon/assets/thumbup-icon.svg')
.addIconSet('core', '/demo-app/icon/assets/core-icon-set.svg')
.registerFontSet('fontawesome', 'fa');
diff --git a/src/main.ts b/src/main.ts
index 8a4595b8410e..078c9ac86b15 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -2,7 +2,7 @@ import {bootstrap} from 'angular2/platform/browser';
import {DemoApp} from './demo-app/demo-app';
import {HTTP_PROVIDERS} from 'angular2/http';
import {ROUTER_PROVIDERS} from 'angular2/router';
-import {MdIconProvider} from './components/icon/icon-provider';
+import {MdIconRegistry} from './components/icon/icon-registry';
import {OVERLAY_CONTAINER_TOKEN} from './core/overlay/overlay';
import {MdLiveAnnouncer} from './core/live-announcer/live-announcer';
import {provide} from 'angular2/core';
@@ -15,6 +15,6 @@ bootstrap(DemoApp, [
MdLiveAnnouncer,
provide(OVERLAY_CONTAINER_TOKEN, {useValue: createOverlayContainer()}),
HTTP_PROVIDERS,
- MdIconProvider,
+ MdIconRegistry,
Renderer,
]);
From 154b5e048a3865995bf1302e033ebafee83febb0 Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Wed, 13 Apr 2016 00:55:07 -0400
Subject: [PATCH 21/37] Fix bug copying icons out of icon sets, and use
Observable.share() correctly so that in-progress HTTP requests can be
coalesced.
---
src/components/icon/icon-registry.ts | 23 +++++++++--------------
src/components/icon/icon.ts | 14 ++++++--------
src/demo-app/icon/icon-demo.html | 1 +
3 files changed, 16 insertions(+), 22 deletions(-)
diff --git a/src/components/icon/icon-registry.ts b/src/components/icon/icon-registry.ts
index 4e8010f85159..81c3aaef554e 100644
--- a/src/components/icon/icon-registry.ts
+++ b/src/components/icon/icon-registry.ts
@@ -172,26 +172,20 @@ export class MdIconRegistry {
}
private _fetchUrl(url: string): Observable {
- // FIXME: This is trying to avoid sending a duplicate request for a URL when there is already
- // a request in progress for that URL. But it's not working; even though we return the cached
- // Observable, a second request is still sent.
- // Observable.share seems like it should work, but doesn't seem to have any effect.
- // (http://xgrommx.github.io/rx-book/content/observable/observable_instance_methods/share.html)
- console.log('*** fetchUrl: ' + url);
+ // Store in-progress fetches to avoid sending a duplicate request for a URL when there is
+ // already a request in progress for that URL. It's necessary to call share() on the
+ // Observable returned by http.get() so that multiple subscribers don't cause multiple XHRs.
if (this._inProgressUrlFetches.has(url)) {
- console.log("*** Using existing request");
return this._inProgressUrlFetches.get(url);
}
- console.log(`*** Sending request for ${url}`);
const req = this._http.get(url)
- .do((response) => {
- console.log(`*** Got response for ${url}`);
- console.log('*** Removing request: ' + url);
+ .map((response) => response.text())
+ .finally(() => {
this._inProgressUrlFetches.delete(url);
})
- .map((response) => response.text());
+ .share();
this._inProgressUrlFetches.set(url, req);
- return req.share();
+ return req;
}
private _loadIconFromConfig(config: IconConfig): Observable {
@@ -235,7 +229,8 @@ export class MdIconRegistry {
// have to create an empty SVG node using innerHTML and append its content.
// http://stackoverflow.com/questions/23003278/svg-innerhtml-in-firefox-can-not-display
const svg = this._svgElementFromString('');
- svg.appendChild(iconNode);
+ // Clone the node so we don't remove it from the parent icon set element.
+ svg.appendChild(iconNode.cloneNode(true));
this._setSvgAttributes(svg, config);
return svg;
}
diff --git a/src/components/icon/icon.ts b/src/components/icon/icon.ts
index cc31d302e2fa..a0dc89738c7b 100644
--- a/src/components/icon/icon.ts
+++ b/src/components/icon/icon.ts
@@ -76,15 +76,13 @@ export class MdIcon implements OnChanges, OnInit, AfterContentChecked {
if (changedInputs.indexOf('svgIcon') != -1 || changedInputs.indexOf('svgSrc') != -1) {
if (this.svgIcon) {
const [namespace, iconName] = this._splitIconName(this.svgIcon);
- this._mdIconRegistry.loadIconFromNamespaceByName(namespace, iconName)
- .subscribe(
- (svg: SVGElement) => this._setSvgElement(svg),
- (err: any) => console.log(`Error retrieving icon: ${err}`));
+ this._mdIconRegistry.loadIconFromNamespaceByName(namespace, iconName).subscribe(
+ (svg: SVGElement) => this._setSvgElement(svg),
+ (err: any) => console.log(`Error retrieving icon: ${err}`));
} else if (this.svgSrc) {
- this._mdIconRegistry.loadIconFromUrl(this.svgSrc)
- .subscribe(
- (svg: SVGElement) => this._setSvgElement(svg),
- (err: any) => console.log(`Error retrieving icon: ${err}`));
+ this._mdIconRegistry.loadIconFromUrl(this.svgSrc).subscribe(
+ (svg: SVGElement) => this._setSvgElement(svg),
+ (err: any) => console.log(`Error retrieving icon: ${err}`));
}
}
if (this._usingFontIcon()) {
diff --git a/src/demo-app/icon/icon-demo.html b/src/demo-app/icon/icon-demo.html
index 0de32df13b23..1dc527be3927 100644
--- a/src/demo-app/icon/icon-demo.html
+++ b/src/demo-app/icon/icon-demo.html
@@ -17,6 +17,7 @@
From icon set:
+
From cef127755deee47dc9c867bd8dc91cd29c6c1428 Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Wed, 13 Apr 2016 01:00:13 -0400
Subject: [PATCH 22/37] Remove unused imports.
---
src/components/icon/icon-registry.ts | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/components/icon/icon-registry.ts b/src/components/icon/icon-registry.ts
index 81c3aaef554e..122fa48bce1e 100644
--- a/src/components/icon/icon-registry.ts
+++ b/src/components/icon/icon-registry.ts
@@ -1,6 +1,6 @@
-import {Injectable, Renderer} from 'angular2/core';
-import {Http, Response, HTTP_PROVIDERS} from 'angular2/http';
-import {AsyncSubject, Observer, Observable} from 'rxjs/Rx';
+import {Injectable} from 'angular2/core';
+import {Http} from 'angular2/http';
+import {Observable} from 'rxjs/Rx';
/**
* Configuration for an icon, possibly including the cached SVG element.
From 30f8ccee78f9c33d5fd92053ab2690ab9dd3f4c6 Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Wed, 13 Apr 2016 20:30:10 -0400
Subject: [PATCH 23/37] Remove unnecessary cloneNode.
---
src/components/icon/icon-registry.ts | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/components/icon/icon-registry.ts b/src/components/icon/icon-registry.ts
index 122fa48bce1e..8f4b8e3970f6 100644
--- a/src/components/icon/icon-registry.ts
+++ b/src/components/icon/icon-registry.ts
@@ -50,12 +50,12 @@ export class MdIconRegistry {
return this;
}
- public addIconSet(setName: string, url: string, viewBoxSize=0): this {
+ public addIconSet(namespace: string, url: string, viewBoxSize=0): this {
const config = new IconConfig(url, viewBoxSize || this._defaultViewBoxSize);
- if (this._iconSetConfigs.has(setName)) {
- this._iconSetConfigs.get(setName).push(config);
+ if (this._iconSetConfigs.has(namespace)) {
+ this._iconSetConfigs.get(namespace).push(config);
} else {
- this._iconSetConfigs.set(setName, [config]);
+ this._iconSetConfigs.set(namespace, [config]);
}
return this;
}
@@ -107,7 +107,7 @@ export class MdIconRegistry {
// We could cache namedSvg in _iconConfigs, but since we have to make a copy every
// time anyway, there's probably not much advantage compared to just always extracting
// it from the icon set.
- return Observable.of(namedIcon.cloneNode(true));
+ return Observable.of(namedIcon);
}
// Not found in any cached icon sets. If there are icon sets with URLs that we haven't
// fetched, fetch them now and look for iconName in the results.
From 73a9a3faf45b99f9292bdbbd6e02b4ec88eb4b13 Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Fri, 15 Apr 2016 16:03:26 -0400
Subject: [PATCH 24/37] Remove custom viewBox size.
---
src/components/icon/icon-registry.ts | 36 ++++++++++------------------
src/components/icon/icon.spec.ts | 9 ++++---
src/demo-app/icon/icon-demo.ts | 2 +-
3 files changed, 18 insertions(+), 29 deletions(-)
diff --git a/src/components/icon/icon-registry.ts b/src/components/icon/icon-registry.ts
index 8f4b8e3970f6..702996a0393c 100644
--- a/src/components/icon/icon-registry.ts
+++ b/src/components/icon/icon-registry.ts
@@ -3,11 +3,11 @@ import {Http} from 'angular2/http';
import {Observable} from 'rxjs/Rx';
/**
- * Configuration for an icon, possibly including the cached SVG element.
+ * Configuration for an icon, including the URL and possibly the cached SVG element.
*/
class IconConfig {
svgElement: SVGElement = null;
- constructor(public url: string, public viewBoxSize: number) {
+ constructor(public url: string) {
}
}
@@ -29,29 +29,31 @@ export class MdIconRegistry {
private _inProgressUrlFetches = new Map>();
- private _defaultViewBoxSize = 24;
private _defaultFontSetClass = 'material-icons';
constructor(private _http: Http) {
}
- public addIcon(iconName: string, url: string, viewBoxSize:number=0): this {
- return this.addIconInNamespace('', iconName, url, viewBoxSize);
+ public addIcon(iconName: string, url: string): this {
+ return this.addIconInNamespace('', iconName, url);
}
- public addIconInNamespace(
- namespace: string, iconName: string, url: string, viewBoxSize:number=0): this {
+ public addIconInNamespace(namespace: string, iconName: string, url: string): this {
let iconSetMap = this._iconConfigs.get(namespace);
if (!iconSetMap) {
iconSetMap = new Map();
this._iconConfigs.set(namespace, iconSetMap);
}
- iconSetMap.set(iconName, new IconConfig(url, viewBoxSize || this._defaultViewBoxSize));
+ iconSetMap.set(iconName, new IconConfig(url));
return this;
}
- public addIconSet(namespace: string, url: string, viewBoxSize=0): this {
- const config = new IconConfig(url, viewBoxSize || this._defaultViewBoxSize);
+ public addIconSet(url: string): this {
+ return this.addIconSetInNamespace('', url);
+ }
+
+ public addIconSetInNamespace(namespace: string, url: string): this {
+ const config = new IconConfig(url);
if (this._iconSetConfigs.has(namespace)) {
this._iconSetConfigs.get(namespace).push(config);
} else {
@@ -65,15 +67,6 @@ export class MdIconRegistry {
return this;
}
- public setDefaultViewBoxSize(size: number) {
- this._defaultViewBoxSize = size;
- return this;
- }
-
- public getDefaultViewBoxSize(): number {
- return this._defaultViewBoxSize;
- }
-
public setDefaultFontSetClass(className: string) {
this._defaultFontSetClass = className;
return this;
@@ -163,7 +156,7 @@ export class MdIconRegistry {
if (this._cachedIconsByUrl.has(url)) {
return Observable.of(this._cachedIconsByUrl.get(url).cloneNode(true));
}
- return this._loadIconFromConfig(new IconConfig(url, this._defaultViewBoxSize))
+ return this._loadIconFromConfig(new IconConfig(url))
.do((svg: SVGElement) => this._cachedIconsByUrl.set(url, svg));
}
@@ -205,7 +198,6 @@ export class MdIconRegistry {
}
private _setSvgAttributes(svg: SVGElement, config: IconConfig) {
- const viewBoxSize = config.viewBoxSize || this._defaultViewBoxSize;
if (!svg.getAttribute('xmlns')) {
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
}
@@ -213,8 +205,6 @@ export class MdIconRegistry {
svg.setAttribute('height', '100%');
svg.setAttribute('width', '100%');
svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
- svg.setAttribute('viewBox',
- svg.getAttribute('viewBox') || ('0 0 ' + viewBoxSize + ' ' + viewBoxSize));
svg.setAttribute('focusable', 'false'); // Disable IE11 default behavior to make SVGs focusable.
}
diff --git a/src/components/icon/icon.spec.ts b/src/components/icon/icon.spec.ts
index f9d2038d956b..c9b9b66cd272 100644
--- a/src/components/icon/icon.spec.ts
+++ b/src/components/icon/icon.spec.ts
@@ -148,7 +148,6 @@ export function main() {
// Default attributes should be set.
expect(svgElement.getAttribute('height')).toBe('100%');
expect(svgElement.getAttribute('height')).toBe('100%');
- expect(svgElement.getAttribute('viewBox')).toBe('0 0 24 24');
// Make sure SVG content is taken from response.
expect(svgElement.children.length).toBe(1);
pathElement = svgElement.children[0];
@@ -227,7 +226,7 @@ export function main() {
});
it('should extract icon from SVG icon set', (done: () => void) => {
- mdIconRegistry.addIconSet('farm', 'farm-set-1.svg');
+ mdIconRegistry.addIconSetInNamespace('farm', 'farm-set-1.svg');
return builder.createAsync(MdIconFromSvgNameTestApp).then((fixture) => {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
@@ -272,9 +271,9 @@ export function main() {
});
it('should allow multiple icon sets in a namespace', (done: () => void) => {
- mdIconRegistry.addIconSet('farm', 'farm-set-1.svg');
- mdIconRegistry.addIconSet('farm', 'farm-set-2.svg');
- mdIconRegistry.addIconSet('arrows', 'arrow-set.svg');
+ mdIconRegistry.addIconSetInNamespace('farm', 'farm-set-1.svg');
+ mdIconRegistry.addIconSetInNamespace('farm', 'farm-set-2.svg');
+ mdIconRegistry.addIconSetInNamespace('arrows', 'arrow-set.svg');
return builder.createAsync(MdIconFromSvgNameTestApp).then((fixture) => {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
diff --git a/src/demo-app/icon/icon-demo.ts b/src/demo-app/icon/icon-demo.ts
index a5d8fe51a68e..cf439d97d8ff 100644
--- a/src/demo-app/icon/icon-demo.ts
+++ b/src/demo-app/icon/icon-demo.ts
@@ -16,7 +16,7 @@ export class IconDemo {
constructor(mdIconRegistry: MdIconRegistry) {
mdIconRegistry
.addIcon('thumb-up', '/demo-app/icon/assets/thumbup-icon.svg')
- .addIconSet('core', '/demo-app/icon/assets/core-icon-set.svg')
+ .addIconSetInNamespace('core', '/demo-app/icon/assets/core-icon-set.svg')
.registerFontSet('fontawesome', 'fa');
}
}
From e87af2e7c189966eac78ad516b00ab4ddd847a0c Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Sun, 17 Apr 2016 22:05:48 -0400
Subject: [PATCH 25/37] Updated for PR comments.
---
src/components/icon/icon-registry.ts | 390 ++++++++++++++-------
src/components/icon/icon.scss | 14 +-
src/components/icon/icon.spec.ts | 34 +-
src/components/icon/icon.ts | 50 ++-
src/demo-app/icon/assets/core-icon-set.svg | 62 ++--
src/demo-app/icon/icon-demo.ts | 6 +-
6 files changed, 366 insertions(+), 190 deletions(-)
diff --git a/src/components/icon/icon-registry.ts b/src/components/icon/icon-registry.ts
index 702996a0393c..7e48af41bd7e 100644
--- a/src/components/icon/icon-registry.ts
+++ b/src/components/icon/icon-registry.ts
@@ -1,59 +1,108 @@
import {Injectable} from 'angular2/core';
+import {BaseException} from 'angular2/src/facade/exceptions';
import {Http} from 'angular2/http';
import {Observable} from 'rxjs/Rx';
+
+/** Exception thrown when attmepting to load an icon with a name that cannot be found. */
+export class MdIconNameNotFoundException extends BaseException {
+ constructor(iconName: string) {
+ super(`Unable to find icon with the name "${name}"`);
+ }
+}
+
/**
- * Configuration for an icon, including the URL and possibly the cached SVG element.
+ * Exception thrown when attmepting to load SVG content that does not contain the expected
+ * tag.
+ */
+export class MdIconSvgTagNotFoundException extends BaseException {
+ constructor() {
+ super(' tag not found');
+ }
+}
+
+/**
+ * Configuration for an icon, including the URL and possibly the cached SVG element.
+ * @internal
*/
-class IconConfig {
+class SvgIconConfig {
svgElement: SVGElement = null;
constructor(public url: string) {
}
}
+/**
+ * Service to register and display icons used by the component.
+ * - Registers icon URLs by namespace and name.
+ * - Registers icon set URLs by namespace.
+ * - Registers aliases for CSS classes, for use with icon fonts.
+ * - Loads icons from URLs and extracts individual icons from icon sets.
+ */
@Injectable()
export class MdIconRegistry {
- // IconConfig objects and cached SVG elements for individual icons.
- // First level of cache is namespace (which is the empty string if not specified).
- // Second level is the icon name within the namespace.
- private _iconConfigs = new Map>();
+ /**
+ * URLs and cached SVG elements for individual icons. Keys are of the format "[namespace]:[icon]".
+ */
+ private _svgIconConfigs = new Map();
- // IconConfig objects and cached SVG elements for icon sets.
- // Multiple icon sets can be registered under the same namespace.
- private _iconSetConfigs = new Map();
+ /**
+ * SvgIconConfig objects and cached SVG elements for icon sets, keyed by namespace.
+ * Multiple icon sets can be registered under the same namespace.
+ */
+ private _iconSetConfigs = new Map();
- // Cache for icons loaded by direct URLs.
+ /**
+ * Cache for icons loaded by direct URLs.
+ */
private _cachedIconsByUrl = new Map();
- private _fontClassNamesByAlias = new Map();
-
+ /**
+ * In-progress icon fetches. Used to coalesce multiple requests to the same URL.
+ */
private _inProgressUrlFetches = new Map>();
+ /**
+ * Map from font identifiers to their CSS class names. Used for icon fonts.
+ */
+ private _fontCssClassesByAlias = new Map();
+
+ /**
+ * The CSS class to apply when an component has no icon name, url, or font specified.
+ * The default 'material-icons' value assumes that the material icon font has been loaded as
+ * described at http://google.github.io/material-design-icons/#icon-font-for-the-web
+ */
private _defaultFontSetClass = 'material-icons';
- constructor(private _http: Http) {
- }
+ constructor(private _http: Http) {}
- public addIcon(iconName: string, url: string): this {
- return this.addIconInNamespace('', iconName, url);
+ /**
+ * Registers an icon by URL in the default namespace.
+ */
+ addSvgIcon(iconName: string, url: string): this {
+ return this.addSvgIconInNamespace('', iconName, url);
}
- public addIconInNamespace(namespace: string, iconName: string, url: string): this {
- let iconSetMap = this._iconConfigs.get(namespace);
- if (!iconSetMap) {
- iconSetMap = new Map();
- this._iconConfigs.set(namespace, iconSetMap);
- }
- iconSetMap.set(iconName, new IconConfig(url));
+ /**
+ * Registers an icon by URL in the specified namespace.
+ */
+ addSvgIconInNamespace(namespace: string, iconName: string, url: string): this {
+ const iconKey = namespace + ':' + iconName;
+ this._svgIconConfigs.set(iconKey, new SvgIconConfig(url));
return this;
}
- public addIconSet(url: string): this {
- return this.addIconSetInNamespace('', url);
+ /**
+ * Registers an icon set by URL in the default namespace.
+ */
+ addSvgIconSet(url: string): this {
+ return this.addSvgIconSetInNamespace('', url);
}
- public addIconSetInNamespace(namespace: string, url: string): this {
- const config = new IconConfig(url);
+ /**
+ * Registers an icon set by URL in the specified namespace.
+ */
+ addSvgIconSetInNamespace(namespace: string, url: string): this {
+ const config = new SvgIconConfig(url);
if (this._iconSetConfigs.has(namespace)) {
this._iconSetConfigs.get(namespace).push(config);
} else {
@@ -62,83 +111,145 @@ export class MdIconRegistry {
return this;
}
- public registerFontSet(alias: string, className?: string): this {
- this._fontClassNamesByAlias.set(alias, className || alias);
+ /**
+ * Defines an alias for a CSS class name to be used for icon fonts. Creating an mdIcon
+ * component with the alias as the fontSet input will cause the class name to be applied
+ * to the element.
+ */
+ registerFontClassAlias(alias: string, className = alias): this {
+ this._fontCssClassesByAlias.set(alias, className);
return this;
}
- public setDefaultFontSetClass(className: string) {
+ /**
+ * Returns the CSS class name associated with the alias by a previous call to
+ * registerFontClassAlias. If no CSS class has been associated, returns the alias unmodified.
+ */
+ classNameForFontAlias(alias: string): string {
+ return this._fontCssClassesByAlias.get(alias) || alias;
+ }
+
+ /**
+ * Sets the CSS class name to be used for icon fonts when an component does not
+ * have a fontSet input value, and is not loading an icon by name or URL.
+ */
+ setDefaultFontSetClass(className: string) {
this._defaultFontSetClass = className;
return this;
}
- public getDefaultFontSetClass(): string {
+ /**
+ * Returns the CSS class name to be used for icon fonts when an component does not
+ * have a fontSet input value, and is not loading an icon by name or URL.
+ */
+ getDefaultFontSetClass(): string {
return this._defaultFontSetClass;
}
- loadIconFromNamespaceByName(namespace: string, iconName: string): Observable {
+ /**
+ * Returns an Observable that produces the icon (as an DOM element) from the given URL.
+ * The response from the URL may be cached so this will not always cause an HTTP request, but
+ * the produced element will always be a new copy of the originally fetched icon. (That is,
+ * it will not contain any modifications made to elements previously returned).
+ */
+ getSvgIconFromUrl(url: string): Observable {
+ if (this._cachedIconsByUrl.has(url)) {
+ return Observable.of(this._cachedIconsByUrl.get(url).cloneNode(true));
+ }
+ return this._loadSvgIconFromConfig(new SvgIconConfig(url))
+ .do(svg => this._cachedIconsByUrl.set(url, svg))
+ .map(svg => svg.cloneNode(true));
+ }
+
+ /**
+ * Returns an Observable that produces the icon (as an DOM element) with the given name
+ * and namespace. The icon must have been previously registered with addIcon or addIconSet;
+ * if not, the Observable will throw an MdIconNameNotFoundException.
+ */
+ getNamedSvgIcon(name: string, namespace = ''): Observable {
// Return (copy of) cached icon if possible.
- if (this._iconConfigs.has(namespace) && this._iconConfigs.get(namespace).has(iconName)) {
- const config = this._iconConfigs.get(namespace).get(iconName);
- if (config.svgElement) {
- // We already have the SVG element for this icon, return a copy.
- return Observable.of(config.svgElement.cloneNode(true));
- } else {
- // Fetch the icon from the config's URL, cache it, and return a copy.
- return this._loadIconFromConfig(config)
- .do((svg: SVGElement) => config.svgElement = svg)
- .map((svg: SVGElement) => svg.cloneNode(true));
- }
+ const iconKey = namespace + ':' + name;
+ if (this._svgIconConfigs.has(iconKey)) {
+ return this._getSvgFromConfig(this._svgIconConfigs.get(iconKey));
}
- // See if we have any icon sets registered for the set name.
+ // See if we have any icon sets registered for the namespace.
const iconSetConfigs = this._iconSetConfigs.get(namespace);
if (iconSetConfigs) {
- // For all the icon set SVG elements we've fetched, see if any contain an icon with the
- // requested name.
- const namedIcon = this._extractIconWithNameFromAnySet(iconName, iconSetConfigs);
- if (namedIcon) {
- // We could cache namedSvg in _iconConfigs, but since we have to make a copy every
- // time anyway, there's probably not much advantage compared to just always extracting
- // it from the icon set.
- return Observable.of(namedIcon);
- }
- // Not found in any cached icon sets. If there are icon sets with URLs that we haven't
- // fetched, fetch them now and look for iconName in the results.
- const iconSetFetchRequests = <[Observable]>[];
- iconSetConfigs.forEach((setConfig) => {
- if (!setConfig.svgElement) {
- iconSetFetchRequests.push(
- this._loadIconSetFromConfig(setConfig)
- .catch((err: any, source: any, caught: any): Observable => {
- // Swallow errors fetching individual URLs so the combined Observable won't
- // necessarily fail.
- console.log(`Loading icon set URL: ${setConfig.url} failed with error: ${err}`);
- return Observable.of(null);
- })
- .do((svg: SVGElement) => {
- // Cache SVG element.
- if (svg) {
- setConfig.svgElement = svg;
- }
- })
- );
- }
- });
- // Fetch all the icon set URLs. When the requests complete, every IconSet should have a
- // cached SVG element (unless the request failed), and we can check again for the icon.
- return Observable.forkJoin(iconSetFetchRequests)
+ return this._getSvgFromIconSetConfigs(name, this._iconSetConfigs.get(namespace));
+ }
+ return Observable.throw(Error(`Unknown icon name: ${name} in namespace: ${namespace}`));
+ }
+
+ /**
+ * Returns the cached icon for a SvgIconConfig if available, or fetches it from its URL if not.
+ */
+ private _getSvgFromConfig(config: SvgIconConfig): Observable {
+ if (config.svgElement) {
+ // We already have the SVG element for this icon, return a copy.
+ return Observable.of(config.svgElement.cloneNode(true));
+ } else {
+ // Fetch the icon from the config's URL, cache it, and return a copy.
+ return this._loadSvgIconFromConfig(config)
+ .do((svg: SVGElement) => config.svgElement = svg)
+ .map((svg: SVGElement) => svg.cloneNode(true));
+ }
+ }
+
+ /**
+ * Attempts to find an icon with the specified name in any of the SVG icon sets.
+ * First searches the available cached icons for a nested element with a matching name, and
+ * if found copies the element to a new element. If not found, fetches all icon sets
+ * that have not been cached, and searches again after all fetches are completed.
+ * The returned Observable produces the SVG element if possible, and throws
+ * MdIconNameNotFoundException if no icon with the specified name can be found.
+ */
+ private _getSvgFromIconSetConfigs(name: string, iconSetConfigs: SvgIconConfig[]):
+ Observable {
+ // For all the icon set SVG elements we've fetched, see if any contain an icon with the
+ // requested name.
+ const namedIcon = this._extractIconWithNameFromAnySet(name, iconSetConfigs);
+ if (namedIcon) {
+ // We could cache namedSvg in _svgIconConfigs, but since we have to make a copy every
+ // time anyway, there's probably not much advantage compared to just always extracting
+ // it from the icon set.
+ return Observable.of(namedIcon);
+ }
+ // Not found in any cached icon sets. If there are icon sets with URLs that we haven't
+ // fetched, fetch them now and look for iconName in the results.
+ const iconSetFetchRequests: Observable[] = iconSetConfigs
+ .filter(iconSetConfig => !iconSetConfig.svgElement)
+ .map(iconSetConfig =>
+ this._loadSvgIconSetFromConfig(iconSetConfig)
+ .catch((err: any, source: any, caught: any): Observable => {
+ // Swallow errors fetching individual URLs so the combined Observable won't
+ // necessarily fail.
+ console.log(`Loading icon set URL: ${iconSetConfig.url} failed: ${err}`);
+ return Observable.of(null);
+ })
+ .do((svg: SVGElement) => {
+ // Cache SVG element.
+ if (svg) {
+ iconSetConfig.svgElement = svg;
+ }
+ }));
+ // Fetch all the icon set URLs. When the requests complete, every IconSet should have a
+ // cached SVG element (unless the request failed), and we can check again for the icon.
+ return Observable.forkJoin(iconSetFetchRequests)
.map((ignoredResults: any) => {
- const foundIcon = this._extractIconWithNameFromAnySet(iconName, iconSetConfigs);
+ const foundIcon = this._extractIconWithNameFromAnySet(name, iconSetConfigs);
if (!foundIcon) {
- throw Error(`Failed to find icon name: ${iconName} in namespace: ${namespace}`);
+ throw new MdIconNameNotFoundException(name);
}
return foundIcon;
});
- }
- return Observable.throw(Error(`Unknown icon name: ${iconName} in namespace: ${namespace}`));
}
- private _extractIconWithNameFromAnySet(iconName: string, setConfigs: IconConfig[]): SVGElement {
+ /**
+ * Searches the cached SVG elements for the given icon sets for a nested icon element whose "id"
+ * tag matches the specified name. If found, copies the nested element to a new SVG element and
+ * returns it. Returns null if no matching element is found.
+ */
+ private _extractIconWithNameFromAnySet(iconName: string, setConfigs: SvgIconConfig[]): SVGElement {
// Iterate backwards, so icon sets added later have precedence.
for (let i = setConfigs.length - 1; i >= 0; i--) {
const config = setConfigs[i];
@@ -152,68 +263,49 @@ export class MdIconRegistry {
return null;
}
- loadIconFromUrl(url: string): Observable {
- if (this._cachedIconsByUrl.has(url)) {
- return Observable.of(this._cachedIconsByUrl.get(url).cloneNode(true));
- }
- return this._loadIconFromConfig(new IconConfig(url))
- .do((svg: SVGElement) => this._cachedIconsByUrl.set(url, svg));
- }
-
- classNameForFontAlias(alias: string): string {
- return this._fontClassNamesByAlias.get(alias) || alias;
- }
-
- private _fetchUrl(url: string): Observable {
- // Store in-progress fetches to avoid sending a duplicate request for a URL when there is
- // already a request in progress for that URL. It's necessary to call share() on the
- // Observable returned by http.get() so that multiple subscribers don't cause multiple XHRs.
- if (this._inProgressUrlFetches.has(url)) {
- return this._inProgressUrlFetches.get(url);
- }
- const req = this._http.get(url)
- .map((response) => response.text())
- .finally(() => {
- this._inProgressUrlFetches.delete(url);
- })
- .share();
- this._inProgressUrlFetches.set(url, req);
- return req;
- }
-
- private _loadIconFromConfig(config: IconConfig): Observable {
+ /**
+ * Loads the content of the icon URL specified in the SvgIconConfig and creates an SVG element
+ * from it.
+ */
+ private _loadSvgIconFromConfig(config: SvgIconConfig): Observable {
return this._fetchUrl(config.url)
.map(svgText => this._createSvgElementForSingleIcon(svgText, config));
}
- private _loadIconSetFromConfig(config: IconConfig): Observable {
+ /**
+ * Loads the content of the icon set URL specified in the SvgIconConfig and creates an SVG element
+ * from it.
+ */
+ private _loadSvgIconSetFromConfig(config: SvgIconConfig): Observable {
return this._fetchUrl(config.url)
.map((svgText) => this._svgElementFromString(svgText));
}
- private _createSvgElementForSingleIcon(responseText: string, config: IconConfig): SVGElement {
+ /**
+ * Creates a DOM element from the given SVG string, and adds default attributes.
+ */
+ private _createSvgElementForSingleIcon(responseText: string, config: SvgIconConfig): SVGElement {
const svg = this._svgElementFromString(responseText);
this._setSvgAttributes(svg, config);
return svg;
}
- private _setSvgAttributes(svg: SVGElement, config: IconConfig) {
- if (!svg.getAttribute('xmlns')) {
- svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
- }
- svg.setAttribute('fit', '');
- svg.setAttribute('height', '100%');
- svg.setAttribute('width', '100%');
- svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
- svg.setAttribute('focusable', 'false'); // Disable IE11 default behavior to make SVGs focusable.
- }
-
+ /**
+ * Searches the cached element of the given SvgIconConfig for a nested icon element whose "id"
+ * tag matches the specified name. If found, copies the nested element to a new SVG element and
+ * returns it. Returns null if no matching element is found.
+ */
private _extractSvgIconFromSet(
- iconSet: SVGElement, iconName: string, config: IconConfig): SVGElement {
+ iconSet: SVGElement, iconName: string, config: SvgIconConfig): SVGElement {
const iconNode = iconSet.querySelector('#' + iconName);
if (!iconNode) {
return null;
}
+ // If the icon node is itself an node, clone and return it directly. If not, set it as
+ // the content of a new node.
+ if (iconNode.tagName.toLowerCase() == 'svg') {
+ return this._setSvgAttributes(iconNode.cloneNode(true), config);
+ }
// createElement('SVG') doesn't work as expected; the DOM ends up with
// the correct nodes, but the SVG content doesn't render. Instead we
// have to create an empty SVG node using innerHTML and append its content.
@@ -221,10 +313,12 @@ export class MdIconRegistry {
const svg = this._svgElementFromString('');
// Clone the node so we don't remove it from the parent icon set element.
svg.appendChild(iconNode.cloneNode(true));
- this._setSvgAttributes(svg, config);
- return svg;
+ return this._setSvgAttributes(svg, config);
}
+ /**
+ * Creates a DOM element from the given SVG string.
+ */
private _svgElementFromString(str: string): SVGElement {
// TODO: Is there a better way than innerHTML? Renderer doesn't appear to have a method for
// creating an element from an HTML string.
@@ -232,8 +326,44 @@ export class MdIconRegistry {
div.innerHTML = str;
const svg = div.querySelector('svg');
if (!svg) {
- throw Error(' tag not found');
+ throw new MdIconSvgTagNotFoundException();
+ }
+ return svg;
+ }
+
+ /**
+ * Sets the default attributes for an SVG element to be used as an icon.
+ */
+ private _setSvgAttributes(svg: SVGElement, config: SvgIconConfig): SVGElement {
+ if (!svg.getAttribute('xmlns')) {
+ svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
}
+ svg.setAttribute('fit', '');
+ svg.setAttribute('height', '100%');
+ svg.setAttribute('width', '100%');
+ svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
+ svg.setAttribute('focusable', 'false'); // Disable IE11 default behavior to make SVGs focusable.
return svg;
}
+
+ /**
+ * Returns an Observable which produces the string contents of the given URL. Results may be
+ * cached, so future calls with the same URL may not cause another HTTP request.
+ */
+ private _fetchUrl(url: string): Observable {
+ // Store in-progress fetches to avoid sending a duplicate request for a URL when there is
+ // already a request in progress for that URL. It's necessary to call share() on the
+ // Observable returned by http.get() so that multiple subscribers don't cause multiple XHRs.
+ if (this._inProgressUrlFetches.has(url)) {
+ return this._inProgressUrlFetches.get(url);
+ }
+ const req = this._http.get(url)
+ .map((response) => response.text())
+ .finally(() => {
+ this._inProgressUrlFetches.delete(url);
+ })
+ .share();
+ this._inProgressUrlFetches.set(url, req);
+ return req;
+ }
}
diff --git a/src/components/icon/icon.scss b/src/components/icon/icon.scss
index 3c1128b3e87b..1fa23d71bc13 100644
--- a/src/components/icon/icon.scss
+++ b/src/components/icon/icon.scss
@@ -9,11 +9,11 @@ This works because we're using ViewEncapsulation.None. If we used the default
encapsulation, the selector would need to be ":host".
*/
md-icon {
- background-repeat: no-repeat;
- display: inline-block;
- fill: currentColor;
- height: $md-icon-size;
- margin: auto;
- vertical-align: middle;
- width: $md-icon-size;
+ background-repeat: no-repeat;
+ display: inline-block;
+ fill: currentColor;
+ height: $md-icon-size;
+ margin: auto;
+ vertical-align: middle;
+ width: $md-icon-size;
}
diff --git a/src/components/icon/icon.spec.ts b/src/components/icon/icon.spec.ts
index c9b9b66cd272..4c7b610436cf 100644
--- a/src/components/icon/icon.spec.ts
+++ b/src/components/icon/icon.spec.ts
@@ -85,8 +85,10 @@ export function main() {
status: 200,
body: `
-
-
+
+
+
+
`,
})));
@@ -96,8 +98,10 @@ export function main() {
status: 200,
body: `
-
-
+
+
+
+
`,
})));
@@ -179,8 +183,8 @@ export function main() {
});
it('should register icon URLs by name', (done: () => void) => {
- mdIconRegistry.addIcon('fluffy', 'cat.svg');
- mdIconRegistry.addIcon('fido', 'dog.svg');
+ mdIconRegistry.addSvgIcon('fluffy', 'cat.svg');
+ mdIconRegistry.addSvgIcon('fido', 'dog.svg');
return builder.createAsync(MdIconFromSvgNameTestApp).then((fixture) => {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
@@ -226,7 +230,7 @@ export function main() {
});
it('should extract icon from SVG icon set', (done: () => void) => {
- mdIconRegistry.addIconSetInNamespace('farm', 'farm-set-1.svg');
+ mdIconRegistry.addSvgIconSetInNamespace('farm', 'farm-set-1.svg');
return builder.createAsync(MdIconFromSvgNameTestApp).then((fixture) => {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
@@ -271,9 +275,9 @@ export function main() {
});
it('should allow multiple icon sets in a namespace', (done: () => void) => {
- mdIconRegistry.addIconSetInNamespace('farm', 'farm-set-1.svg');
- mdIconRegistry.addIconSetInNamespace('farm', 'farm-set-2.svg');
- mdIconRegistry.addIconSetInNamespace('arrows', 'arrow-set.svg');
+ mdIconRegistry.addSvgIconSetInNamespace('farm', 'farm-set-1.svg');
+ mdIconRegistry.addSvgIconSetInNamespace('farm', 'farm-set-2.svg');
+ mdIconRegistry.addSvgIconSetInNamespace('arrows', 'arrow-set.svg');
return builder.createAsync(MdIconFromSvgNameTestApp).then((fixture) => {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
@@ -326,8 +330,8 @@ export function main() {
describe('custom fonts', () => {
it('should apply CSS classes for custom font and icon', (done: () => void) => {
- mdIconRegistry.registerFontSet('f1', 'font1');
- mdIconRegistry.registerFontSet('f2');
+ mdIconRegistry.registerFontClassAlias('f1', 'font1');
+ mdIconRegistry.registerFontClassAlias('f2');
return builder.createAsync(MdIconCustomFontCssTestApp).then((fixture) => {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
@@ -469,3 +473,9 @@ class MdIconFromSvgNameTestApp {
ariaLabel: string = null;
iconName = '';
}
+
+
+// tests
+// modification of icon doesn't propagate
+// as icon set children
+// icon name not found
\ No newline at end of file
diff --git a/src/components/icon/icon.ts b/src/components/icon/icon.ts
index a0dc89738c7b..05eea52a0943 100644
--- a/src/components/icon/icon.ts
+++ b/src/components/icon/icon.ts
@@ -1,5 +1,6 @@
import {
AfterContentChecked,
+ ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Directive,
@@ -19,6 +20,40 @@ import {
MdIconRegistry,
} from './icon-registry';
+
+/**
+ * Component to display an icon. It can be used in the following ways:
+ * - Specify the svgSrc input to load an SVG icon from a URL. The SVG content is directly inlined
+ * as a child of the component, so that CSS styles can easily be applied to it.
+ * The URL is loaded via an XMLHttpRequest, so it must be on the same domain as the page or its
+ * server must be configured to allow cross-domain requests.
+ * Example:
+ *
+ *
+ * - Specify the svgIcon input to load an SVG icon from a URL previously registered with the
+ * addSvgIcon, addSvgIconInNamespace, addSvgIconSet, or addSvgIconSetInNamespace methods of
+ * MdIconRegistry. If the svgIcon value contains a colon it is assumed to be in the format
+ * "[namespace]:[name]", if not the value will be the name of an icon in the default namespace.
+ * Examples:
+ *
+ *
+ *
+ * - Use a font ligature as an icon by putting the ligature text in the content of the
+ * component. By default the Material icons font is used as described at
+ * http://google.github.io/material-design-icons/#icon-font-for-the-web. You can specify an
+ * alternate font by setting the fontSet input to either the CSS class to apply to use the
+ * desired font, or to an alias previously registered with MdIconRegistry.registerFontClassAlias.
+ * Examples:
+ * home
+ * sun
+ *
+ * - Specify a font glyph to be included via CSS rules by setting the fontSet input to specify the
+ * font, and the fontIcon input to specify the icon. (Typically the fontIcon will specify a class
+ * which causes the icon glyph to be displayed via a :before selector, for example
+ *
+ * Example:
+ *
+ */
@Component({
template: '',
selector: 'md-icon',
@@ -28,6 +63,7 @@ import {
},
directives: [NgClass],
encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MdIcon implements OnChanges, OnInit, AfterContentChecked {
@Input() svgSrc: string;
@@ -63,11 +99,11 @@ export class MdIcon implements OnChanges, OnInit, AfterContentChecked {
if (!iconName) {
return ['', ''];
}
- const sepIndex = this.svgIcon.indexOf(':');
- if (sepIndex == -1) {
+ const separatorIndex = this.svgIcon.indexOf(':');
+ if (separatorIndex == -1) {
return ['', iconName];
}
- return [iconName.substring(0, sepIndex), iconName.substring(sepIndex + 1)];
+ return [iconName.substring(0, separatorIndex), iconName.substring(separatorIndex + 1)];
}
ngOnChanges(changes: { [propertyName: string]: SimpleChange }) {
@@ -76,12 +112,12 @@ export class MdIcon implements OnChanges, OnInit, AfterContentChecked {
if (changedInputs.indexOf('svgIcon') != -1 || changedInputs.indexOf('svgSrc') != -1) {
if (this.svgIcon) {
const [namespace, iconName] = this._splitIconName(this.svgIcon);
- this._mdIconRegistry.loadIconFromNamespaceByName(namespace, iconName).subscribe(
- (svg: SVGElement) => this._setSvgElement(svg),
+ this._mdIconRegistry.getNamedSvgIcon(iconName, namespace).subscribe(
+ svg => this._setSvgElement(svg),
(err: any) => console.log(`Error retrieving icon: ${err}`));
} else if (this.svgSrc) {
- this._mdIconRegistry.loadIconFromUrl(this.svgSrc).subscribe(
- (svg: SVGElement) => this._setSvgElement(svg),
+ this._mdIconRegistry.getSvgIconFromUrl(this.svgSrc).subscribe(
+ svg => this._setSvgElement(svg),
(err: any) => console.log(`Error retrieving icon: ${err}`));
}
}
diff --git a/src/demo-app/icon/assets/core-icon-set.svg b/src/demo-app/icon/assets/core-icon-set.svg
index 60c9e7b0ee33..ee86795a9a20 100644
--- a/src/demo-app/icon/assets/core-icon-set.svg
+++ b/src/demo-app/icon/assets/core-icon-set.svg
@@ -1,33 +1,33 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/demo-app/icon/icon-demo.ts b/src/demo-app/icon/icon-demo.ts
index cf439d97d8ff..6f13fd0c87ff 100644
--- a/src/demo-app/icon/icon-demo.ts
+++ b/src/demo-app/icon/icon-demo.ts
@@ -15,8 +15,8 @@ export class IconDemo {
constructor(mdIconRegistry: MdIconRegistry) {
mdIconRegistry
- .addIcon('thumb-up', '/demo-app/icon/assets/thumbup-icon.svg')
- .addIconSetInNamespace('core', '/demo-app/icon/assets/core-icon-set.svg')
- .registerFontSet('fontawesome', 'fa');
+ .addSvgIcon('thumb-up', '/demo-app/icon/assets/thumbup-icon.svg')
+ .addSvgIconSetInNamespace('core', '/demo-app/icon/assets/core-icon-set.svg')
+ .registerFontClassAlias('fontawesome', 'fa');
}
}
From 062307e95c8a3922d9446ff933279a195744a714 Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Sun, 17 Apr 2016 22:08:40 -0400
Subject: [PATCH 26/37] formatting fixes
---
src/demo-app/icon/icon-demo.scss | 4 ++--
src/demo-app/icon/icon-demo.ts | 22 ++++++++++------------
2 files changed, 12 insertions(+), 14 deletions(-)
diff --git a/src/demo-app/icon/icon-demo.scss b/src/demo-app/icon/icon-demo.scss
index 176309cf06e4..25617ffeae3f 100644
--- a/src/demo-app/icon/icon-demo.scss
+++ b/src/demo-app/icon/icon-demo.scss
@@ -1,7 +1,7 @@
md-icon {
- color: purple;
+ color: purple;
}
md-icon.green {
- color: green;
+ color: green;
}
diff --git a/src/demo-app/icon/icon-demo.ts b/src/demo-app/icon/icon-demo.ts
index 6f13fd0c87ff..f0ab0be3d71f 100644
--- a/src/demo-app/icon/icon-demo.ts
+++ b/src/demo-app/icon/icon-demo.ts
@@ -3,20 +3,18 @@ import {MdIcon} from '../../components/icon/icon';
import {MdIconRegistry} from '../../components/icon/icon-registry';
@Component({
- selector: 'icon-demo',
- templateUrl: 'demo-app/icon/icon-demo.html',
- styleUrls: ['demo-app/icon/icon-demo.css'],
- directives: [MdIcon],
- viewProviders: [MdIconRegistry],
- encapsulation: ViewEncapsulation.None,
+ selector: 'icon-demo',
+ templateUrl: 'demo-app/icon/icon-demo.html',
+ styleUrls: ['demo-app/icon/icon-demo.css'],
+ directives: [MdIcon],
+ viewProviders: [MdIconRegistry],
+ encapsulation: ViewEncapsulation.None,
})
export class IconDemo {
- showAndroid = true;
-
constructor(mdIconRegistry: MdIconRegistry) {
- mdIconRegistry
- .addSvgIcon('thumb-up', '/demo-app/icon/assets/thumbup-icon.svg')
- .addSvgIconSetInNamespace('core', '/demo-app/icon/assets/core-icon-set.svg')
- .registerFontClassAlias('fontawesome', 'fa');
+ mdIconRegistry
+ .addSvgIcon('thumb-up', '/demo-app/icon/assets/thumbup-icon.svg')
+ .addSvgIconSetInNamespace('core', '/demo-app/icon/assets/core-icon-set.svg')
+ .registerFontClassAlias('fontawesome', 'fa');
}
}
From d3682b771c701a6fef688eca21ada52b6962bb5a Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Sun, 17 Apr 2016 22:36:36 -0400
Subject: [PATCH 27/37] Fix tslint errors.
---
src/components/icon/icon-registry.ts | 9 +++++----
src/components/icon/icon.spec.ts | 20 ++------------------
src/components/icon/icon.ts | 2 --
3 files changed, 7 insertions(+), 24 deletions(-)
diff --git a/src/components/icon/icon-registry.ts b/src/components/icon/icon-registry.ts
index 7e48af41bd7e..50988b4cbe20 100644
--- a/src/components/icon/icon-registry.ts
+++ b/src/components/icon/icon-registry.ts
@@ -177,7 +177,7 @@ export class MdIconRegistry {
if (iconSetConfigs) {
return this._getSvgFromIconSetConfigs(name, this._iconSetConfigs.get(namespace));
}
- return Observable.throw(Error(`Unknown icon name: ${name} in namespace: ${namespace}`));
+ return Observable.throw(new MdIconNameNotFoundException(iconKey));
}
/**
@@ -249,10 +249,11 @@ export class MdIconRegistry {
* tag matches the specified name. If found, copies the nested element to a new SVG element and
* returns it. Returns null if no matching element is found.
*/
- private _extractIconWithNameFromAnySet(iconName: string, setConfigs: SvgIconConfig[]): SVGElement {
+ private _extractIconWithNameFromAnySet(iconName: string, iconSetConfigs: SvgIconConfig[]):
+ SVGElement {
// Iterate backwards, so icon sets added later have precedence.
- for (let i = setConfigs.length - 1; i >= 0; i--) {
- const config = setConfigs[i];
+ for (let i = iconSetConfigs.length - 1; i >= 0; i--) {
+ const config = iconSetConfigs[i];
if (config.svgElement) {
const foundIcon = this._extractSvgIconFromSet(config.svgElement, iconName, config);
if (foundIcon) {
diff --git a/src/components/icon/icon.spec.ts b/src/components/icon/icon.spec.ts
index 4c7b610436cf..2282f8e92d97 100644
--- a/src/components/icon/icon.spec.ts
+++ b/src/components/icon/icon.spec.ts
@@ -9,22 +9,13 @@ import {
} from 'angular2/testing';
import {
HTTP_PROVIDERS,
- BaseRequestOptions,
Response,
ResponseOptions,
- Http,
XHRBackend} from 'angular2/http';
-import {
- MockBackend,
- MockConnection} from 'angular2/http/testing';
+import {MockBackend} from 'angular2/http/testing';
import {
provide,
Component} from 'angular2/core';
-import {By} from 'angular2/platform/browser';
-import {
- TEST_BROWSER_PLATFORM_PROVIDERS,
- TEST_BROWSER_APPLICATION_PROVIDERS
-} from 'angular2/platform/testing/browser';
import {MdIcon} from './icon';
import {MdIconRegistry} from './icon-registry';
@@ -191,7 +182,7 @@ export function main() {
let svgElement: any;
let pathElement: any;
- testComponent.iconName= 'fido';
+ testComponent.iconName = 'fido';
fixture.detectChanges();
expect(mdIconElement.children.length).toBe(1);
svgElement = mdIconElement.children[0];
@@ -203,7 +194,6 @@ export function main() {
// The aria label should be taken from the icon name.
expect(mdIconElement.getAttribute('aria-label')).toBe('fido');
-
// Change the icon, and the SVG element should be replaced.
testComponent.iconName = 'fluffy';
fixture.detectChanges();
@@ -473,9 +463,3 @@ class MdIconFromSvgNameTestApp {
ariaLabel: string = null;
iconName = '';
}
-
-
-// tests
-// modification of icon doesn't propagate
-// as icon set children
-// icon name not found
\ No newline at end of file
diff --git a/src/components/icon/icon.ts b/src/components/icon/icon.ts
index 05eea52a0943..ae1c22bcce2c 100644
--- a/src/components/icon/icon.ts
+++ b/src/components/icon/icon.ts
@@ -3,9 +3,7 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
- Directive,
ElementRef,
- HostBinding,
Input,
OnChanges,
OnInit,
From 8e2ff62a8611a48193382dbdce8356bfc535766e Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Mon, 18 Apr 2016 01:39:52 -0400
Subject: [PATCH 28/37] Factor out common test assertions, and add tests for
elements in icon sets.
---
src/components/icon/icon.spec.ts | 194 ++++++++++++++++++++-----------
src/components/icon/icon.ts | 11 +-
2 files changed, 132 insertions(+), 73 deletions(-)
diff --git a/src/components/icon/icon.spec.ts b/src/components/icon/icon.spec.ts
index 2282f8e92d97..5a415a4aa126 100644
--- a/src/components/icon/icon.spec.ts
+++ b/src/components/icon/icon.spec.ts
@@ -20,8 +20,30 @@ import {
import {MdIcon} from './icon';
import {MdIconRegistry} from './icon-registry';
+/** Returns the CSS classes assigned to an element as a sorted array. */
const sortedClassNames = (elem: Element) => elem.className.split(' ').sort();
+/**
+ * Verifies that an element contains a single child element, and returns that child.
+ */
+const verifyAndGetSingleSvgChild = (element: any): any => {
+ expect(element.children.length).toBe(1);
+ const svgChild = element.children[0];
+ expect(svgChild.tagName.toLowerCase()).toBe('svg');
+ return svgChild;
+};
+
+/**
+ * Verifies that an element contains a single child element whose "d" attribute has
+ * the specified value.
+ */
+const verifyPathChildElement = (element: any, attributeValue: string) => {
+ expect(element.children.length).toBe(1);
+ const pathElement = element.children[0];
+ expect(pathElement.tagName.toLowerCase()).toBe('path');
+ expect(pathElement.getAttribute('d')).toBe(attributeValue);
+};
+
export function main() {
describe('MdIcon', () => {
@@ -90,8 +112,8 @@ export function main() {
body: `
-
-
+
+
`,
@@ -132,41 +154,29 @@ export function main() {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
let svgElement: any;
- let pathElement: any;
testComponent.iconUrl = 'cat.svg';
fixture.detectChanges();
// An element should have been added as a child of .
- expect(mdIconElement.children.length).toBe(1);
- svgElement = mdIconElement.children[0];
- expect(svgElement.tagName.toLowerCase()).toBe('svg');
+ svgElement = verifyAndGetSingleSvgChild(mdIconElement);
// Default attributes should be set.
expect(svgElement.getAttribute('height')).toBe('100%');
expect(svgElement.getAttribute('height')).toBe('100%');
// Make sure SVG content is taken from response.
- expect(svgElement.children.length).toBe(1);
- pathElement = svgElement.children[0];
- expect(pathElement.tagName.toLowerCase()).toBe('path');
- expect(pathElement.getAttribute('d')).toBe('meow');
+ verifyPathChildElement(svgElement, 'meow');
// Change the icon, and the SVG element should be replaced.
testComponent.iconUrl = 'dog.svg';
fixture.detectChanges();
- expect(mdIconElement.children.length).toBe(1);
- svgElement = mdIconElement.children[0];
- expect(svgElement.tagName.toLowerCase()).toBe('svg');
- expect(svgElement.children.length).toBe(1);
- pathElement = svgElement.children[0];
- expect(pathElement.tagName.toLowerCase()).toBe('path');
- expect(pathElement.getAttribute('d')).toBe('woof');
+ svgElement = verifyAndGetSingleSvgChild(mdIconElement);
+ verifyPathChildElement(svgElement, 'woof');
expect(httpRequestUrls).toEqual(['cat.svg', 'dog.svg']);
// Using an icon from a previously loaded URL should not cause another HTTP request.
testComponent.iconUrl = 'cat.svg';
fixture.detectChanges();
- expect(mdIconElement.children.length).toBe(1);
- pathElement = mdIconElement.querySelector('svg path');
- expect(pathElement.getAttribute('d')).toBe('meow');
+ svgElement = verifyAndGetSingleSvgChild(mdIconElement);
+ verifyPathChildElement(svgElement, 'meow');
expect(httpRequestUrls).toEqual(['cat.svg', 'dog.svg']);
done();
@@ -179,40 +189,28 @@ export function main() {
return builder.createAsync(MdIconFromSvgNameTestApp).then((fixture) => {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
- let svgElement: any;
- let pathElement: any;
+ let svgElement: SVGElement;
testComponent.iconName = 'fido';
fixture.detectChanges();
- expect(mdIconElement.children.length).toBe(1);
- svgElement = mdIconElement.children[0];
- expect(svgElement.tagName.toLowerCase()).toBe('svg');
- expect(svgElement.children.length).toBe(1);
- pathElement = svgElement.children[0];
- expect(pathElement.tagName.toLowerCase()).toBe('path');
- expect(pathElement.getAttribute('d')).toBe('woof');
+ svgElement = verifyAndGetSingleSvgChild(mdIconElement);
+ verifyPathChildElement(svgElement, 'woof');
// The aria label should be taken from the icon name.
expect(mdIconElement.getAttribute('aria-label')).toBe('fido');
// Change the icon, and the SVG element should be replaced.
testComponent.iconName = 'fluffy';
fixture.detectChanges();
- expect(mdIconElement.children.length).toBe(1);
- svgElement = mdIconElement.children[0];
- expect(svgElement.tagName.toLowerCase()).toBe('svg');
- expect(svgElement.children.length).toBe(1);
- pathElement = svgElement.children[0];
- expect(pathElement.tagName.toLowerCase()).toBe('path');
- expect(pathElement.getAttribute('d')).toBe('meow');
+ svgElement = verifyAndGetSingleSvgChild(mdIconElement);
+ verifyPathChildElement(svgElement, 'meow');
expect(mdIconElement.getAttribute('aria-label')).toBe('fluffy');
expect(httpRequestUrls).toEqual(['dog.svg', 'cat.svg']);
// Using an icon from a previously loaded URL should not cause another HTTP request.
testComponent.iconName = 'fido';
fixture.detectChanges();
- expect(mdIconElement.children.length).toBe(1);
- pathElement = mdIconElement.querySelector('svg path');
- expect(pathElement.getAttribute('d')).toBe('woof');
+ svgElement = verifyAndGetSingleSvgChild(mdIconElement);
+ verifyPathChildElement(svgElement, 'woof');
expect(httpRequestUrls).toEqual(['dog.svg', 'cat.svg']);
done();
@@ -225,39 +223,31 @@ export function main() {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
let svgElement: any;
- let pathElement: any;
let svgChild: any;
testComponent.iconName = 'farm:pig';
fixture.detectChanges();
+
expect(mdIconElement.children.length).toBe(1);
- svgElement = mdIconElement.children[0];
- expect(svgElement.tagName.toLowerCase()).toBe('svg');
+ svgElement = verifyAndGetSingleSvgChild(mdIconElement);
expect(svgElement.children.length).toBe(1);
svgChild = svgElement.children[0];
// The first child should be the element.
expect(svgChild.tagName.toLowerCase()).toBe('g');
expect(svgChild.getAttribute('id')).toBe('pig');
- expect(svgChild.children.length).toBe(1);
- pathElement = svgChild.children[0];
- expect(pathElement.tagName.toLowerCase()).toBe('path');
- expect(pathElement.getAttribute('d')).toBe('oink');
+ verifyPathChildElement(svgChild, 'oink');
// The aria label should be taken from the icon name (without the icon set portion).
expect(mdIconElement.getAttribute('aria-label')).toBe('pig');
// Change the icon, and the SVG element should be replaced.
testComponent.iconName = 'farm:cow';
fixture.detectChanges();
- expect(mdIconElement.children.length).toBe(1);
- svgElement = mdIconElement.children[0];
+ svgElement = verifyAndGetSingleSvgChild(mdIconElement);
svgChild = svgElement.children[0];
// The first child should be the element.
expect(svgChild.tagName.toLowerCase()).toBe('g');
expect(svgChild.getAttribute('id')).toBe('cow');
- expect(svgChild.children.length).toBe(1);
- pathElement = svgChild.children[0];
- expect(pathElement.tagName.toLowerCase()).toBe('path');
- expect(pathElement.getAttribute('d')).toBe('moo');
+ verifyPathChildElement(svgChild, 'moo');
expect(mdIconElement.getAttribute('aria-label')).toBe('cow');
done();
@@ -272,24 +262,19 @@ export function main() {
const testComponent = fixture.debugElement.componentInstance;
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
let svgElement: any;
- let pathElement: any;
let svgChild: any;
testComponent.iconName = 'farm:pig';
fixture.detectChanges();
- expect(mdIconElement.children.length).toBe(1);
- svgElement = mdIconElement.children[0];
- expect(svgElement.tagName.toLowerCase()).toBe('svg');
+ svgElement = verifyAndGetSingleSvgChild(mdIconElement);
expect(svgElement.children.length).toBe(1);
svgChild = svgElement.children[0];
- // The first child should be the element.
+ // The child should be the element.
expect(svgChild.tagName.toLowerCase()).toBe('g');
expect(svgChild.getAttribute('id')).toBe('pig');
expect(svgChild.children.length).toBe(1);
- pathElement = svgChild.children[0];
- expect(pathElement.tagName.toLowerCase()).toBe('path');
- expect(pathElement.getAttribute('d')).toBe('oink');
- // The aria label should be taken from the icon name (without the icon set portion).
+ verifyPathChildElement(svgChild, 'oink');
+ // The aria label should be taken from the icon name (without the namespace).
expect(mdIconElement.getAttribute('aria-label')).toBe('pig');
// Both icon sets registered in the 'farm' namespace should have been fetched.
@@ -300,22 +285,99 @@ export function main() {
// and no additional HTTP request should be made.
testComponent.iconName = 'farm:cow';
fixture.detectChanges();
- expect(mdIconElement.children.length).toBe(1);
- svgElement = mdIconElement.children[0];
+ svgElement = verifyAndGetSingleSvgChild(mdIconElement);
svgChild = svgElement.children[0];
// The first child should be the element.
expect(svgChild.tagName.toLowerCase()).toBe('g');
expect(svgChild.getAttribute('id')).toBe('cow');
expect(svgChild.children.length).toBe(1);
- pathElement = svgChild.children[0];
- expect(pathElement.tagName.toLowerCase()).toBe('path');
- expect(pathElement.getAttribute('d')).toBe('moo moo');
+ verifyPathChildElement(svgChild, 'moo moo');
expect(mdIconElement.getAttribute('aria-label')).toBe('cow');
expect(httpRequestUrls.sort()).toEqual(['farm-set-1.svg', 'farm-set-2.svg']);
done();
});
});
+
+ it('should not wrap elements in icon sets in another svg tag', (done: () => void) => {
+ mdIconRegistry.addSvgIconSet('arrow-set.svg');
+ return builder.createAsync(MdIconFromSvgNameTestApp).then((fixture) => {
+ const testComponent = fixture.debugElement.componentInstance;
+ const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
+ let svgElement: any;
+
+ testComponent.iconName = 'left-arrow';
+ fixture.detectChanges();
+ // arrow-set.svg stores its icons as nested elements, so they should be used
+ // directly and not wrapped in an outer tag like the elements in other sets.
+ svgElement = verifyAndGetSingleSvgChild(mdIconElement);
+ verifyPathChildElement(svgElement, 'left');
+ expect(mdIconElement.getAttribute('aria-label')).toBe('left-arrow');
+
+ done();
+ });
+ });
+
+ it('should return unmodified copies of icons from URLs', (done: () => void) => {
+ return builder.createAsync(MdIconFromSvgUrlTestApp).then((fixture) => {
+ const testComponent = fixture.debugElement.componentInstance;
+ const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
+ let svgElement: any;
+
+ testComponent.iconUrl = 'cat.svg';
+ fixture.detectChanges();
+ svgElement = verifyAndGetSingleSvgChild(mdIconElement);
+ verifyPathChildElement(svgElement, 'meow');
+ // Modify the SVG element by setting a viewBox attribute.
+ svgElement.setAttribute('viewBox', '0 0 100 100');
+
+ // Switch to a different icon.
+ testComponent.iconUrl = 'dog.svg';
+ fixture.detectChanges();
+ svgElement = verifyAndGetSingleSvgChild(mdIconElement);
+ verifyPathChildElement(svgElement, 'woof');
+
+ // Switch back to the first icon. The viewBox attribute should not be present.
+ testComponent.iconUrl = 'cat.svg';
+ fixture.detectChanges();
+ svgElement = verifyAndGetSingleSvgChild(mdIconElement);
+ verifyPathChildElement(svgElement, 'meow');
+ expect(svgElement.getAttribute('viewBox')).toBeFalsy();
+
+ done();
+ });
+ });
+
+ it('should return unmodified copies of icons from icon sets', (done: () => void) => {
+ mdIconRegistry.addSvgIconSet('arrow-set.svg');
+ return builder.createAsync(MdIconFromSvgNameTestApp).then((fixture) => {
+ const testComponent = fixture.debugElement.componentInstance;
+ const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
+ let svgElement: any;
+
+ testComponent.iconName = 'left-arrow';
+ fixture.detectChanges();
+ svgElement = verifyAndGetSingleSvgChild(mdIconElement);
+ verifyPathChildElement(svgElement, 'left');
+ // Modify the SVG element by setting a viewBox attribute.
+ svgElement.setAttribute('viewBox', '0 0 100 100');
+
+ // Switch to a different icon.
+ testComponent.iconName = 'right-arrow';
+ fixture.detectChanges();
+ svgElement = verifyAndGetSingleSvgChild(mdIconElement);
+ verifyPathChildElement(svgElement, 'right');
+
+ // Switch back to the first icon. The viewBox attribute should not be present.
+ testComponent.iconName = 'left-arrow';
+ fixture.detectChanges();
+ svgElement = verifyAndGetSingleSvgChild(mdIconElement);
+ verifyPathChildElement(svgElement, 'left');
+ expect(svgElement.getAttribute('viewBox')).toBeFalsy();
+
+ done();
+ });
+ });
});
describe('custom fonts', () => {
diff --git a/src/components/icon/icon.ts b/src/components/icon/icon.ts
index ae1c22bcce2c..cd73728681f1 100644
--- a/src/components/icon/icon.ts
+++ b/src/components/icon/icon.ts
@@ -11,12 +11,9 @@ import {
SimpleChange,
ViewEncapsulation,
} from 'angular2/core';
-
import {NgClass} from 'angular2/common';
-import {
- MdIconRegistry,
-} from './icon-registry';
+import {MdIconRegistry} from './icon-registry';
/**
@@ -46,9 +43,9 @@ import {
* sun
*
* - Specify a font glyph to be included via CSS rules by setting the fontSet input to specify the
- * font, and the fontIcon input to specify the icon. (Typically the fontIcon will specify a class
- * which causes the icon glyph to be displayed via a :before selector, for example
- *
+ * font, and the fontIcon input to specify the icon. Typically the fontIcon will specify a
+ * CSS class which causes the glyph to be displayed via a :before selector, as in
+ * https://fortawesome.github.io/Font-Awesome/examples/
* Example:
*
*/
From 4b02d2728e40ed0a4d7c1e58ba072173104dc1e9 Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Mon, 18 Apr 2016 21:58:41 -0400
Subject: [PATCH 29/37] Fix tests for Edge and IE11 (element.children bad,
element.childNodes good).
---
src/components/icon/icon.spec.ts | 48 ++++++++++++++++----------------
1 file changed, 24 insertions(+), 24 deletions(-)
diff --git a/src/components/icon/icon.spec.ts b/src/components/icon/icon.spec.ts
index 5a415a4aa126..c5cd1ab1e992 100644
--- a/src/components/icon/icon.spec.ts
+++ b/src/components/icon/icon.spec.ts
@@ -27,21 +27,21 @@ const sortedClassNames = (elem: Element) => elem.className.split(' ').sort();
* Verifies that an element contains a single child element, and returns that child.
*/
const verifyAndGetSingleSvgChild = (element: any): any => {
- expect(element.children.length).toBe(1);
- const svgChild = element.children[0];
+ expect(element.childNodes.length).toBe(1);
+ const svgChild = element.childNodes[0];
expect(svgChild.tagName.toLowerCase()).toBe('svg');
return svgChild;
};
/**
- * Verifies that an element contains a single child element whose "d" attribute has
+ * Verifies that an element contains a single child element whose "id" attribute has
* the specified value.
*/
const verifyPathChildElement = (element: any, attributeValue: string) => {
- expect(element.children.length).toBe(1);
- const pathElement = element.children[0];
+ expect(element.childNodes.length).toBe(1);
+ const pathElement = element.childNodes[0];
expect(pathElement.tagName.toLowerCase()).toBe('path');
- expect(pathElement.getAttribute('d')).toBe(attributeValue);
+ expect(pathElement.getAttribute('id')).toBe(attributeValue);
};
export function main() {
@@ -73,13 +73,13 @@ export function main() {
case 'cat.svg':
connection.mockRespond(new Response(new ResponseOptions({
status: 200,
- body: '',
+ body: '',
})));
break;
case 'dog.svg':
connection.mockRespond(new Response(new ResponseOptions({
status: 200,
- body: '',
+ body: '',
})));
break;
case 'farm-set-1.svg':
@@ -87,8 +87,8 @@ export function main() {
status: 200,
body: `
-
-
+
+
`,
})));
@@ -99,8 +99,8 @@ export function main() {
body: `
-
-
+
+
`,
@@ -112,8 +112,8 @@ export function main() {
body: `
-
-
+
+
`,
@@ -228,10 +228,10 @@ export function main() {
testComponent.iconName = 'farm:pig';
fixture.detectChanges();
- expect(mdIconElement.children.length).toBe(1);
+ expect(mdIconElement.childNodes.length).toBe(1);
svgElement = verifyAndGetSingleSvgChild(mdIconElement);
- expect(svgElement.children.length).toBe(1);
- svgChild = svgElement.children[0];
+ expect(svgElement.childNodes.length).toBe(1);
+ svgChild = svgElement.childNodes[0];
// The first child should be the element.
expect(svgChild.tagName.toLowerCase()).toBe('g');
expect(svgChild.getAttribute('id')).toBe('pig');
@@ -243,7 +243,7 @@ export function main() {
testComponent.iconName = 'farm:cow';
fixture.detectChanges();
svgElement = verifyAndGetSingleSvgChild(mdIconElement);
- svgChild = svgElement.children[0];
+ svgChild = svgElement.childNodes[0];
// The first child should be the element.
expect(svgChild.tagName.toLowerCase()).toBe('g');
expect(svgChild.getAttribute('id')).toBe('cow');
@@ -267,12 +267,12 @@ export function main() {
testComponent.iconName = 'farm:pig';
fixture.detectChanges();
svgElement = verifyAndGetSingleSvgChild(mdIconElement);
- expect(svgElement.children.length).toBe(1);
- svgChild = svgElement.children[0];
+ expect(svgElement.childNodes.length).toBe(1);
+ svgChild = svgElement.childNodes[0];
// The child should be the element.
expect(svgChild.tagName.toLowerCase()).toBe('g');
expect(svgChild.getAttribute('id')).toBe('pig');
- expect(svgChild.children.length).toBe(1);
+ expect(svgChild.childNodes.length).toBe(1);
verifyPathChildElement(svgChild, 'oink');
// The aria label should be taken from the icon name (without the namespace).
expect(mdIconElement.getAttribute('aria-label')).toBe('pig');
@@ -281,16 +281,16 @@ export function main() {
expect(httpRequestUrls.sort()).toEqual(['farm-set-1.svg', 'farm-set-2.svg']);
// Change the icon name to one that appears in both icon sets. The icon from the set that
- // was registered last should be used (with 'd' attribute of 'moo moo' instead of 'moo'),
+ // was registered last should be used (with id attribute of 'moo moo' instead of 'moo'),
// and no additional HTTP request should be made.
testComponent.iconName = 'farm:cow';
fixture.detectChanges();
svgElement = verifyAndGetSingleSvgChild(mdIconElement);
- svgChild = svgElement.children[0];
+ svgChild = svgElement.childNodes[0];
// The first child should be the element.
expect(svgChild.tagName.toLowerCase()).toBe('g');
expect(svgChild.getAttribute('id')).toBe('cow');
- expect(svgChild.children.length).toBe(1);
+ expect(svgChild.childNodes.length).toBe(1);
verifyPathChildElement(svgChild, 'moo moo');
expect(mdIconElement.getAttribute('aria-label')).toBe('cow');
expect(httpRequestUrls.sort()).toEqual(['farm-set-1.svg', 'farm-set-2.svg']);
From 437ab0ce18cdbfa63be2747d10a2048f73297f5a Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Tue, 19 Apr 2016 18:31:21 -0400
Subject: [PATCH 30/37] In-progress updates for PR comments.
---
src/components/icon/icon-registry.ts | 50 +++++++++++-----------------
src/components/icon/icon.ts | 32 ++++++++++++------
2 files changed, 42 insertions(+), 40 deletions(-)
diff --git a/src/components/icon/icon-registry.ts b/src/components/icon/icon-registry.ts
index 50988b4cbe20..37b6248e0986 100644
--- a/src/components/icon/icon-registry.ts
+++ b/src/components/icon/icon-registry.ts
@@ -31,6 +31,9 @@ class SvgIconConfig {
}
}
+/** Returns the cache key to use for an icon namespace and name. */
+const iconKey = (namespace: string, name: string) => namespace + ':' + name;
+
/**
* Service to register and display icons used by the component.
* - Registers icon URLs by namespace and name.
@@ -51,19 +54,13 @@ export class MdIconRegistry {
*/
private _iconSetConfigs = new Map();
- /**
- * Cache for icons loaded by direct URLs.
- */
+ /** Cache for icons loaded by direct URLs. */
private _cachedIconsByUrl = new Map();
- /**
- * In-progress icon fetches. Used to coalesce multiple requests to the same URL.
- */
+ /** In-progress icon fetches. Used to coalesce multiple requests to the same URL. */
private _inProgressUrlFetches = new Map>();
- /**
- * Map from font identifiers to their CSS class names. Used for icon fonts.
- */
+ /** Map from font identifiers to their CSS class names. Used for icon fonts. */
private _fontCssClassesByAlias = new Map();
/**
@@ -75,32 +72,24 @@ export class MdIconRegistry {
constructor(private _http: Http) {}
- /**
- * Registers an icon by URL in the default namespace.
- */
+ /** Registers an icon by URL in the default namespace. */
addSvgIcon(iconName: string, url: string): this {
return this.addSvgIconInNamespace('', iconName, url);
}
- /**
- * Registers an icon by URL in the specified namespace.
- */
+ /** Registers an icon by URL in the specified namespace. */
addSvgIconInNamespace(namespace: string, iconName: string, url: string): this {
- const iconKey = namespace + ':' + iconName;
- this._svgIconConfigs.set(iconKey, new SvgIconConfig(url));
+ const key = iconKey(namespace, iconName);
+ this._svgIconConfigs.set(key, new SvgIconConfig(url));
return this;
}
- /**
- * Registers an icon set by URL in the default namespace.
- */
+ /** Registers an icon set by URL in the default namespace. */
addSvgIconSet(url: string): this {
return this.addSvgIconSetInNamespace('', url);
}
- /**
- * Registers an icon set by URL in the specified namespace.
- */
+ /** Registers an icon set by URL in the specified namespace. */
addSvgIconSetInNamespace(namespace: string, url: string): this {
const config = new SvgIconConfig(url);
if (this._iconSetConfigs.has(namespace)) {
@@ -168,16 +157,16 @@ export class MdIconRegistry {
*/
getNamedSvgIcon(name: string, namespace = ''): Observable {
// Return (copy of) cached icon if possible.
- const iconKey = namespace + ':' + name;
- if (this._svgIconConfigs.has(iconKey)) {
- return this._getSvgFromConfig(this._svgIconConfigs.get(iconKey));
+ const key = iconKey(namespace, name);
+ if (this._svgIconConfigs.has(key)) {
+ return this._getSvgFromConfig(this._svgIconConfigs.get(key));
}
// See if we have any icon sets registered for the namespace.
const iconSetConfigs = this._iconSetConfigs.get(namespace);
if (iconSetConfigs) {
return this._getSvgFromIconSetConfigs(name, this._iconSetConfigs.get(namespace));
}
- return Observable.throw(new MdIconNameNotFoundException(iconKey));
+ return Observable.throw(new MdIconNameNotFoundException(key));
}
/**
@@ -190,8 +179,8 @@ export class MdIconRegistry {
} else {
// Fetch the icon from the config's URL, cache it, and return a copy.
return this._loadSvgIconFromConfig(config)
- .do((svg: SVGElement) => config.svgElement = svg)
- .map((svg: SVGElement) => svg.cloneNode(true));
+ .do(svg => config.svgElement = svg)
+ .map(svg => svg.cloneNode(true));
}
}
@@ -226,7 +215,7 @@ export class MdIconRegistry {
console.log(`Loading icon set URL: ${iconSetConfig.url} failed: ${err}`);
return Observable.of(null);
})
- .do((svg: SVGElement) => {
+ .do(svg => {
// Cache SVG element.
if (svg) {
iconSetConfig.svgElement = svg;
@@ -278,6 +267,7 @@ export class MdIconRegistry {
* from it.
*/
private _loadSvgIconSetFromConfig(config: SvgIconConfig): Observable {
+ // TODO: Document that icons should only be loaded from trusted sources.
return this._fetchUrl(config.url)
.map((svgText) => this._svgElementFromString(svgText));
}
diff --git a/src/components/icon/icon.ts b/src/components/icon/icon.ts
index cd73728681f1..bf7afa625877 100644
--- a/src/components/icon/icon.ts
+++ b/src/components/icon/icon.ts
@@ -1,7 +1,6 @@
import {
AfterContentChecked,
ChangeDetectionStrategy,
- ChangeDetectorRef,
Component,
ElementRef,
Input,
@@ -12,10 +11,18 @@ import {
ViewEncapsulation,
} from 'angular2/core';
import {NgClass} from 'angular2/common';
+import {BaseException} from 'angular2/src/facade/exceptions';
import {MdIconRegistry} from './icon-registry';
+/** Exception thrown when an invalid icon name is passed to an md-icon component. */
+export class MdIconInvalidNameException extends BaseException {
+ constructor(iconName: string) {
+ super(`Invalid icon name: "${name}"`);
+ }
+}
+
/**
* Component to display an icon. It can be used in the following ways:
* - Specify the svgSrc input to load an SVG icon from a URL. The SVG content is directly inlined
@@ -67,7 +74,7 @@ export class MdIcon implements OnChanges, OnInit, AfterContentChecked {
@Input() fontIcon: string;
@Input() alt: string;
- @Input('aria-label') ariaLabelFromParent: string = '';
+ @Input('aria-label') hostAriaLabel: string = '';
private _previousFontSetClass: string;
private _previousFontIconClass: string;
@@ -75,7 +82,6 @@ export class MdIcon implements OnChanges, OnInit, AfterContentChecked {
constructor(
private _element: ElementRef,
private _renderer: Renderer,
- private _changeDetectorRef: ChangeDetectorRef,
private _mdIconRegistry: MdIconRegistry) {
}
@@ -85,20 +91,27 @@ export class MdIcon implements OnChanges, OnInit, AfterContentChecked {
* The separator for the two fields is ':'. If there is no separator, an empty
* string is returned for the icon set and the entire value is returned for
* the icon name. If the argument is falsy, returns an array of two empty strings.
+ * Throws a MdIconInvalidNameException if the name contains two or more ':' separators.
* Examples:
* 'social:cake' -> ['social', 'cake']
* 'penguin' -> ['', 'penguin']
* null -> ['', '']
+ * 'a:b:c' -> (throws MdIconInvalidNameException)
*/
private _splitIconName(iconName: string): [string, string] {
if (!iconName) {
return ['', ''];
}
- const separatorIndex = this.svgIcon.indexOf(':');
- if (separatorIndex == -1) {
- return ['', iconName];
+ const parts = iconName.split(':');
+ switch (parts.length) {
+ case 1:
+ // Use default namespace.
+ return ['', parts[0]];
+ case 2:
+ return <[string, string]>parts;
+ default:
+ throw new MdIconInvalidNameException(iconName);
}
- return [iconName.substring(0, separatorIndex), iconName.substring(separatorIndex + 1)];
}
ngOnChanges(changes: { [propertyName: string]: SimpleChange }) {
@@ -140,7 +153,6 @@ export class MdIcon implements OnChanges, OnInit, AfterContentChecked {
const ariaLabel = this._getAriaLabel();
if (ariaLabel) {
this._renderer.setElementAttribute(this._element.nativeElement, 'aria-label', ariaLabel);
- this._changeDetectorRef.detectChanges();
}
}
@@ -149,7 +161,7 @@ export class MdIcon implements OnChanges, OnInit, AfterContentChecked {
// reasonable value from the alt attribute, font icon name, SVG icon name, or (for ligatures)
// the text content of the directive.
const label =
- this.ariaLabelFromParent ||
+ this.hostAriaLabel ||
this.alt ||
this.fontIcon ||
this._splitIconName(this.svgIcon)[1];
@@ -163,7 +175,7 @@ export class MdIcon implements OnChanges, OnInit, AfterContentChecked {
return text;
}
}
- // Warn here?
+ // TODO: Warn here in dev mode.
return null;
}
From 364391a7b315f1549fd3bbf2e248ee5c0b60c24c Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Tue, 19 Apr 2016 19:18:14 -0400
Subject: [PATCH 31/37] More PR comments.
---
src/components/icon/icon-registry.ts | 10 +++++-----
src/components/icon/icon.spec.ts | 8 ++++----
src/components/icon/icon.ts | 6 +++---
src/demo-app/icon/icon-demo.scss | 4 ++--
src/demo-app/icon/icon-demo.ts | 2 +-
5 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/src/components/icon/icon-registry.ts b/src/components/icon/icon-registry.ts
index 37b6248e0986..e28a84d1dfbe 100644
--- a/src/components/icon/icon-registry.ts
+++ b/src/components/icon/icon-registry.ts
@@ -4,7 +4,7 @@ import {Http} from 'angular2/http';
import {Observable} from 'rxjs/Rx';
-/** Exception thrown when attmepting to load an icon with a name that cannot be found. */
+/** Exception thrown when attempting to load an icon with a name that cannot be found. */
export class MdIconNameNotFoundException extends BaseException {
constructor(iconName: string) {
super(`Unable to find icon with the name "${name}"`);
@@ -12,7 +12,7 @@ export class MdIconNameNotFoundException extends BaseException {
}
/**
- * Exception thrown when attmepting to load SVG content that does not contain the expected
+ * Exception thrown when attempting to load SVG content that does not contain the expected
* tag.
*/
export class MdIconSvgTagNotFoundException extends BaseException {
@@ -122,7 +122,7 @@ export class MdIconRegistry {
* Sets the CSS class name to be used for icon fonts when an component does not
* have a fontSet input value, and is not loading an icon by name or URL.
*/
- setDefaultFontSetClass(className: string) {
+ setDefaultFontSetClass(className: string): this {
this._defaultFontSetClass = className;
return this;
}
@@ -164,7 +164,7 @@ export class MdIconRegistry {
// See if we have any icon sets registered for the namespace.
const iconSetConfigs = this._iconSetConfigs.get(namespace);
if (iconSetConfigs) {
- return this._getSvgFromIconSetConfigs(name, this._iconSetConfigs.get(namespace));
+ return this._getSvgFromIconSetConfigs(name, iconSetConfigs);
}
return Observable.throw(new MdIconNameNotFoundException(key));
}
@@ -198,7 +198,7 @@ export class MdIconRegistry {
// requested name.
const namedIcon = this._extractIconWithNameFromAnySet(name, iconSetConfigs);
if (namedIcon) {
- // We could cache namedSvg in _svgIconConfigs, but since we have to make a copy every
+ // We could cache namedIcon in _svgIconConfigs, but since we have to make a copy every
// time anyway, there's probably not much advantage compared to just always extracting
// it from the icon set.
return Observable.of(namedIcon);
diff --git a/src/components/icon/icon.spec.ts b/src/components/icon/icon.spec.ts
index c5cd1ab1e992..67262dabaad7 100644
--- a/src/components/icon/icon.spec.ts
+++ b/src/components/icon/icon.spec.ts
@@ -26,9 +26,9 @@ const sortedClassNames = (elem: Element) => elem.className.split(' ').sort();
/**
* Verifies that an element contains a single child element, and returns that child.
*/
-const verifyAndGetSingleSvgChild = (element: any): any => {
+const verifyAndGetSingleSvgChild = (element: SVGElement): any => {
expect(element.childNodes.length).toBe(1);
- const svgChild = element.childNodes[0];
+ const svgChild = element.childNodes[0];
expect(svgChild.tagName.toLowerCase()).toBe('svg');
return svgChild;
};
@@ -37,9 +37,9 @@ const verifyAndGetSingleSvgChild = (element: any): any => {
* Verifies that an element contains a single child element whose "id" attribute has
* the specified value.
*/
-const verifyPathChildElement = (element: any, attributeValue: string) => {
+const verifyPathChildElement = (element: Element, attributeValue: string) => {
expect(element.childNodes.length).toBe(1);
- const pathElement = element.childNodes[0];
+ const pathElement = element.childNodes[0];
expect(pathElement.tagName.toLowerCase()).toBe('path');
expect(pathElement.getAttribute('id')).toBe(attributeValue);
};
diff --git a/src/components/icon/icon.ts b/src/components/icon/icon.ts
index bf7afa625877..07c856116fdb 100644
--- a/src/components/icon/icon.ts
+++ b/src/components/icon/icon.ts
@@ -184,10 +184,10 @@ export class MdIcon implements OnChanges, OnInit, AfterContentChecked {
}
private _setSvgElement(svg: SVGElement) {
- // Can we use Renderer here somehow?
const layoutElement = this._element.nativeElement;
- layoutElement.innerHTML = '';
- layoutElement.appendChild(svg);
+ // Remove existing child nodes and add the new SVG element.
+ this._renderer.detachView(Array.from(layoutElement.childNodes));
+ this._renderer.projectNodes(layoutElement, [svg]);
}
private _updateFontIconClasses() {
diff --git a/src/demo-app/icon/icon-demo.scss b/src/demo-app/icon/icon-demo.scss
index 25617ffeae3f..27663d1390c7 100644
--- a/src/demo-app/icon/icon-demo.scss
+++ b/src/demo-app/icon/icon-demo.scss
@@ -1,7 +1,7 @@
-md-icon {
+md-icon-demo md-icon {
color: purple;
}
-md-icon.green {
+md-icon-demo md-icon.green {
color: green;
}
diff --git a/src/demo-app/icon/icon-demo.ts b/src/demo-app/icon/icon-demo.ts
index f0ab0be3d71f..ae29dbedbd2b 100644
--- a/src/demo-app/icon/icon-demo.ts
+++ b/src/demo-app/icon/icon-demo.ts
@@ -3,7 +3,7 @@ import {MdIcon} from '../../components/icon/icon';
import {MdIconRegistry} from '../../components/icon/icon-registry';
@Component({
- selector: 'icon-demo',
+ selector: 'md-icon-demo',
templateUrl: 'demo-app/icon/icon-demo.html',
styleUrls: ['demo-app/icon/icon-demo.css'],
directives: [MdIcon],
From 6dc1af293b87cb8393a50aace6bfce5f8fd930b8 Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Tue, 19 Apr 2016 20:18:19 -0400
Subject: [PATCH 32/37] Remove Renderer.detachView call since it doesn't work
in IE11.
---
src/components/icon/icon.ts | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/components/icon/icon.ts b/src/components/icon/icon.ts
index 07c856116fdb..8a295f54871b 100644
--- a/src/components/icon/icon.ts
+++ b/src/components/icon/icon.ts
@@ -186,7 +186,9 @@ export class MdIcon implements OnChanges, OnInit, AfterContentChecked {
private _setSvgElement(svg: SVGElement) {
const layoutElement = this._element.nativeElement;
// Remove existing child nodes and add the new SVG element.
- this._renderer.detachView(Array.from(layoutElement.childNodes));
+ // We would use renderer.detachView(Array.from(layoutElement.childNodes)) here,
+ // but it fails in IE11.
+ layoutElement.innerHTML = '';
this._renderer.projectNodes(layoutElement, [svg]);
}
From 796d37f7f8a1d593db1b21e289c32ff42e36aad2 Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Tue, 19 Apr 2016 22:55:25 -0400
Subject: [PATCH 33/37] comments
---
src/components/icon/icon-registry.ts | 3 ++-
src/components/icon/icon.ts | 2 +-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/components/icon/icon-registry.ts b/src/components/icon/icon-registry.ts
index e28a84d1dfbe..d9246074b4ab 100644
--- a/src/components/icon/icon-registry.ts
+++ b/src/components/icon/icon-registry.ts
@@ -7,7 +7,7 @@ import {Observable} from 'rxjs/Rx';
/** Exception thrown when attempting to load an icon with a name that cannot be found. */
export class MdIconNameNotFoundException extends BaseException {
constructor(iconName: string) {
- super(`Unable to find icon with the name "${name}"`);
+ super(`Unable to find icon with the name "${iconName}"`);
}
}
@@ -300,6 +300,7 @@ export class MdIconRegistry {
// createElement('SVG') doesn't work as expected; the DOM ends up with
// the correct nodes, but the SVG content doesn't render. Instead we
// have to create an empty SVG node using innerHTML and append its content.
+ // Elements created using DOMParser.parseFromString have the same problem.
// http://stackoverflow.com/questions/23003278/svg-innerhtml-in-firefox-can-not-display
const svg = this._svgElementFromString('');
// Clone the node so we don't remove it from the parent icon set element.
diff --git a/src/components/icon/icon.ts b/src/components/icon/icon.ts
index 8a295f54871b..44d4bd4b5047 100644
--- a/src/components/icon/icon.ts
+++ b/src/components/icon/icon.ts
@@ -187,7 +187,7 @@ export class MdIcon implements OnChanges, OnInit, AfterContentChecked {
const layoutElement = this._element.nativeElement;
// Remove existing child nodes and add the new SVG element.
// We would use renderer.detachView(Array.from(layoutElement.childNodes)) here,
- // but it fails in IE11.
+ // but it fails in IE11: https://github.com/angular/angular/issues/6327
layoutElement.innerHTML = '';
this._renderer.projectNodes(layoutElement, [svg]);
}
From a4ea23eb37e7a92cb2f6b557d7c3db4aee1f6af4 Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Tue, 19 Apr 2016 23:21:16 -0400
Subject: [PATCH 34/37] Move test SVGs to separate file.
---
src/components/icon/fake-svgs.ts | 58 ++++++++++++++++++++++++++++++++
src/components/icon/icon.spec.ts | 57 ++-----------------------------
2 files changed, 61 insertions(+), 54 deletions(-)
create mode 100644 src/components/icon/fake-svgs.ts
diff --git a/src/components/icon/fake-svgs.ts b/src/components/icon/fake-svgs.ts
new file mode 100644
index 000000000000..791996498b63
--- /dev/null
+++ b/src/components/icon/fake-svgs.ts
@@ -0,0 +1,58 @@
+import {
+ Response,
+ ResponseOptions} from 'angular2/http';
+
+/**
+ * Fake URLs and associated SVG documents used by tests.
+ */
+const FAKE_SVGS = (() => {
+ const svgs = new Map();
+ svgs.set('cat.svg',
+ '');
+
+ svgs.set('dog.svg',
+ '');
+
+ svgs.set('farm-set-1.svg', `
+
+
+
+
+
+
+ `);
+
+ svgs.set('farm-set-2.svg', `
+
+
+
+
+
+
+ `);
+
+ svgs.set('arrow-set.svg', `
+
+
+
+
+
+
+ `);
+
+ return svgs;
+})();
+
+/**
+ * Returns an HTTP response for a fake SVG URL.
+ */
+export function getFakeSvgHttpResponse(url: string) {
+ if (FAKE_SVGS.has(url)) {
+ return new Response(new ResponseOptions({
+ status: 200,
+ body: FAKE_SVGS.get(url),
+ }));
+ } else {
+ return new Response(new ResponseOptions({status: 404}));
+ }
+}
diff --git a/src/components/icon/icon.spec.ts b/src/components/icon/icon.spec.ts
index 67262dabaad7..fbb58be72ddd 100644
--- a/src/components/icon/icon.spec.ts
+++ b/src/components/icon/icon.spec.ts
@@ -9,8 +9,6 @@ import {
} from 'angular2/testing';
import {
HTTP_PROVIDERS,
- Response,
- ResponseOptions,
XHRBackend} from 'angular2/http';
import {MockBackend} from 'angular2/http/testing';
import {
@@ -19,6 +17,7 @@ import {
import {MdIcon} from './icon';
import {MdIconRegistry} from './icon-registry';
+import {getFakeSvgHttpResponse} from './fake-svgs';
/** Returns the CSS classes assigned to an element as a sorted array. */
const sortedClassNames = (elem: Element) => elem.className.split(' ').sort();
@@ -63,63 +62,13 @@ export function main() {
(tcb: TestComponentBuilder, mir: MdIconRegistry, mockBackend: MockBackend) => {
builder = tcb;
mdIconRegistry = mir;
- // Set fake responses for various SVG URLs.
// Keep track of requests so we can verify caching behavior.
+ // Return responses for the SVGs defined in fake-svgs.ts.
httpRequestUrls = [];
mockBackend.connections.subscribe((connection: any) => {
const url = connection.request.url;
httpRequestUrls.push(url);
- switch (url) {
- case 'cat.svg':
- connection.mockRespond(new Response(new ResponseOptions({
- status: 200,
- body: '',
- })));
- break;
- case 'dog.svg':
- connection.mockRespond(new Response(new ResponseOptions({
- status: 200,
- body: '',
- })));
- break;
- case 'farm-set-1.svg':
- connection.mockRespond(new Response(new ResponseOptions({
- status: 200,
- body: `
-
-
-
-
- `,
- })));
- break;
- case 'farm-set-2.svg':
- connection.mockRespond(new Response(new ResponseOptions({
- status: 200,
- body: `
-
-
-
-
-
-
- `,
- })));
- break;
- case 'arrow-set.svg':
- connection.mockRespond(new Response(new ResponseOptions({
- status: 200,
- body: `
-
-
-
-
-
-
- `,
- })));
- break;
- }
+ connection.mockRespond(getFakeSvgHttpResponse(url));
});
}));
From df6415bb3ba0ec776453a1129f7d957706981a40 Mon Sep 17 00:00:00 2001
From: Brian Nenninger
Date: Wed, 20 Apr 2016 15:33:26 -0400
Subject: [PATCH 35/37] Use for menu icon.
---
src/demo-app/demo-app.html | 2 +-
src/demo-app/demo-app.scss | 2 +-
src/demo-app/demo-app.ts | 2 ++
3 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/demo-app/demo-app.html b/src/demo-app/demo-app.html
index 474c860682df..e7964b39bc58 100644
--- a/src/demo-app/demo-app.html
+++ b/src/demo-app/demo-app.html
@@ -22,7 +22,7 @@