Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

md-icon #281

Merged
merged 45 commits into from
Apr 23, 2016
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
62e82fe
Start of md-icon directive and service.
Mar 23, 2016
8a188e4
Return SVG element instead of string, set default styles.
Mar 23, 2016
8b6f5ff
API and caching cleanup.
Mar 24, 2016
9518ca2
var->const
Mar 24, 2016
ec761da
Make MdIconProvider available at top level so it's shared by all comp…
Mar 24, 2016
776b755
Icon set support.
Mar 24, 2016
d88144e
Font support, various refactorings.
dozingcat Mar 27, 2016
159d53d
Merge remote-tracking branch 'origin/master' into mdicon
Mar 28, 2016
e423582
Rename directive bindings.
Mar 29, 2016
3ce1334
Merge branch 'master' into mdicon
dozingcat Apr 3, 2016
22a92e1
test
dozingcat Apr 3, 2016
0e6f396
Injecting test HTTP classes.
dozingcat Apr 4, 2016
014be77
start of tests
dozingcat Apr 4, 2016
ea3beea
HTTP tests.
dozingcat Apr 5, 2016
72d167f
Improved change detection and added more tests.
dozingcat Apr 7, 2016
53aeed2
Icon namespaces, addIcon and addIconSet are now additive.
dozingcat Apr 8, 2016
7390015
cleanup
Apr 8, 2016
86d86aa
Merge branch 'master' into mdicon
Apr 8, 2016
17f40c5
fix tests
Apr 8, 2016
87dd4ac
Remove local changes.
Apr 8, 2016
82bd25e
remove unused entry
Apr 8, 2016
e4d568f
more tests
dozingcat Apr 10, 2016
af2f94b
MdIconProvider->MdIconRegistry
dozingcat Apr 10, 2016
154b5e0
Fix bug copying icons out of icon sets, and use Observable.share() co…
dozingcat Apr 13, 2016
cef1277
Remove unused imports.
dozingcat Apr 13, 2016
30f8cce
Remove unnecessary cloneNode.
dozingcat Apr 14, 2016
73a9a3f
Remove custom viewBox size.
Apr 15, 2016
03bcbed
Merge remote-tracking branch 'upstream/master' into mdicon
Apr 15, 2016
e87af2e
Updated for PR comments.
dozingcat Apr 18, 2016
062307e
formatting fixes
dozingcat Apr 18, 2016
d3682b7
Fix tslint errors.
dozingcat Apr 18, 2016
8e2ff62
Factor out common test assertions, and add tests for <svg> elements i…
dozingcat Apr 18, 2016
4b02d27
Fix tests for Edge and IE11 (element.children bad, element.childNodes…
dozingcat Apr 19, 2016
195acd3
Merge remote-tracking branch 'upstream/master' into mdicon
dozingcat Apr 19, 2016
6391cdb
Merge remote-tracking branch 'upstream/master' into mdicon
Apr 19, 2016
437ab0c
In-progress updates for PR comments.
Apr 19, 2016
364391a
More PR comments.
Apr 19, 2016
6dc1af2
Remove Renderer.detachView call since it doesn't work in IE11.
dozingcat Apr 20, 2016
796d37f
comments
dozingcat Apr 20, 2016
a4ea23e
Move test SVGs to separate file.
dozingcat Apr 20, 2016
df6415b
Use <md-icon> for menu icon.
Apr 20, 2016
04b23fa
Use watched:false in karma.config.ts.
Apr 20, 2016
37d8362
Merge remote-tracking branch 'upstream/master' into mdicon
Apr 22, 2016
6927d52
Merge remote-tracking branch 'upstream/master' into mdicon
Apr 22, 2016
08e8cbd
PR comments for test component bindings and demo styles.
dozingcat Apr 23, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
370 changes: 370 additions & 0 deletions src/components/icon/icon-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,370 @@
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. */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: attmepting --> attempting

export class MdIconNameNotFoundException extends BaseException {
constructor(iconName: string) {
super(`Unable to find icon with the name "${name}"`);
}
}

/**
* Exception thrown when attmepting to load SVG content that does not contain the expected
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: attmepting --> attempting

* <svg> tag.
*/
export class MdIconSvgTagNotFoundException extends BaseException {
constructor() {
super('<svg> tag not found');
}
}

/**
* Configuration for an icon, including the URL and possibly the cached SVG element.
* @internal
*/
class SvgIconConfig {
svgElement: SVGElement = null;
constructor(public url: string) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: empty functions can close on the same line

constructor(public url: string) { }

(here and elsewhere)

}
}

/**
* Service to register and display icons used by the <md-icon> 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need description on MdIconRegistry

/**
* URLs and cached SVG elements for individual icons. Keys are of the format "[namespace]:[icon]".
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional: I'm cool with single-lining description blocks when they fit on one line.

/** Like this */

private _svgIconConfigs = new Map<string, SvgIconConfig>();

/**
* 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<string, SvgIconConfig[]>();

/**
* Cache for icons loaded by direct URLs.
*/
private _cachedIconsByUrl = new Map<string, SVGElement>();

/**
* In-progress icon fetches. Used to coalesce multiple requests to the same URL.
*/
private _inProgressUrlFetches = new Map<string, Observable<string>>();

/**
* Map from font identifiers to their CSS class names. Used for icon fonts.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accurate to say Used for icon fonts that are not ligature based. ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could have a custom icon font that uses ligatures and still set an alias for its CSS class and use it like <md-icon fontSet="myfont">home</md-icon>, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right, never mind me.

*/
private _fontCssClassesByAlias = new Map<string, string>();

/**
* The CSS class to apply when an <md-icon> 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';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need descriptions for these members


constructor(private _http: Http) {}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add method descriptions

/**
* 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.
*/
addSvgIconInNamespace(namespace: string, iconName: string, url: string): this {
const iconKey = namespace + ':' + iconName;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have one place for this, something like iconKey(namespace, name). It can be a function inside the module so that it doesn't have to be qualified.

this._svgIconConfigs.set(iconKey, new SvgIconConfig(url));
return this;
}

/**
* 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.
*/
addSvgIconSetInNamespace(namespace: string, url: string): this {
const config = new SvgIconConfig(url);
if (this._iconSetConfigs.has(namespace)) {
this._iconSetConfigs.get(namespace).push(config);
} else {
this._iconSetConfigs.set(namespace, [config]);
}
return this;
}

/**
* 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 <md-icon> element.
*/
registerFontClassAlias(alias: string, className = alias): this {
this._fontCssClassesByAlias.set(alias, className);
return this;
}

/**
* 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 <md-icon> component does not
* have a fontSet input value, and is not loading an icon by name or URL.
*/
setDefaultFontSetClass(className: string) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing return value type (this)

this._defaultFontSetClass = className;
return this;
}

/**
* Returns the CSS class name to be used for icon fonts when an <md-icon> component does not
* have a fontSet input value, and is not loading an icon by name or URL.
*/
getDefaultFontSetClass(): string {
return this._defaultFontSetClass;
}

/**
* Returns an Observable that produces the icon (as an <svg> 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<SVGElement> {
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 <svg> 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<SVGElement> {
// Return (copy of) cached icon if possible.
const iconKey = namespace + ':' + name;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: Use template string here rather than concat? i.e. ${namespace}:${name}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually think the concatenation is cleaner in this particular case.

if (this._svgIconConfigs.has(iconKey)) {
return this._getSvgFromConfig(this._svgIconConfigs.get(iconKey));
}
// 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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason we're not using the iconSetConfigs const we just created? e.g.

return this._getSvgFromIconSetConfigs(name, iconSetConfigs);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No reason at all, changed.

}
return Observable.throw(new MdIconNameNotFoundException(iconKey));
}

/**
* Returns the cached icon for a SvgIconConfig if available, or fetches it from its URL if not.
*/
private _getSvgFromConfig(config: SvgIconConfig): Observable<SVGElement> {
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)
Copy link
Member

@jelbourn jelbourn Apr 19, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the explicit SVGElement type necessary here? TS should be able to infer it from the generic.

.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 <svg> 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<SVGElement> {
// 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<SVGElement>[] = iconSetConfigs
.filter(iconSetConfig => !iconSetConfig.svgElement)
.map(iconSetConfig =>
this._loadSvgIconSetFromConfig(iconSetConfig)
.catch((err: any, source: any, caught: any): Observable<SVGElement> => {
// 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional nitpick: collapse onto one line?

if (svg) iconSetConfig.svgElement = svg;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually prefer always having braces and newlines.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really? Don't like the waste of vertical space :-p But completely subjective!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reluctantly agree with @jelbourn. (On a related note, I'm very pleased with the 100-char line length).

}
}));
// 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(name, iconSetConfigs);
if (!foundIcon) {
throw new MdIconNameNotFoundException(name);
}
return foundIcon;
});
}

/**
* 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, iconSetConfigs: SvgIconConfig[]):
SVGElement {
// Iterate backwards, so icon sets added later have precedence.
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) {
return foundIcon;
}
}
}
return null;
}

/**
* Loads the content of the icon URL specified in the SvgIconConfig and creates an SVG element
* from it.
*/
private _loadSvgIconFromConfig(config: SvgIconConfig): Observable<SVGElement> {
return this._fetchUrl(config.url)
.map(svgText => this._createSvgElementForSingleIcon(svgText, config));
}

/**
* Loads the content of the icon set URL specified in the SvgIconConfig and creates an SVG element
* from it.
*/
private _loadSvgIconSetFromConfig(config: SvgIconConfig): Observable<SVGElement> {
return this._fetchUrl(config.url)
.map((svgText) => this._svgElementFromString(svgText));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My XSS sense is tingling here. I'm trying to imagine a situation where an app would use md-icon to load an icon based on user input, but nothing is really coming to mind. Could you add a TODO for something like "Document that icons should only be loaded from trusted sources."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that felt sketchy to me also. Although I don't think it's any worse than ngMaterial 1.x.

}

/**
* 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;
}

/**
* 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: SvgIconConfig): SVGElement {
const iconNode = iconSet.querySelector('#' + iconName);
if (!iconNode) {
return null;
}
// If the icon node is itself an <svg> node, clone and return it directly. If not, set it as
// the content of a new <svg> node.
if (iconNode.tagName.toLowerCase() == 'svg') {
return this._setSvgAttributes(<SVGElement>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.
// http://stackoverflow.com/questions/23003278/svg-innerhtml-in-firefox-can-not-display
const svg = this._svgElementFromString('<svg></svg>');
// Clone the node so we don't remove it from the parent icon set element.
svg.appendChild(iconNode.cloneNode(true));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this making any assumptions about the element type of iconNode?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, although I added a check so that elements are used directly rather than wrapped in another . Should we require it to be or ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's good- checking for a direct svg element was what I was thinking.

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
Copy link
Contributor

@hansl hansl Apr 19, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p = new DOMParser();
p.parseFromString('<a>hello<b>world</a>', 'text/html').body

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, I had never seen this API before.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neat, and it specifically supports SVG documents. Requires IE10 per https://developer.mozilla.org/en-US/docs/Web/API/DOMParser, is that ok?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we only support IE11+

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. We support IE11 and up. You might want to create the DOMParser once.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've played with this for a while, but it appears that using DOMParser to create an element has the same problem as document.createElement('svg'), where the DOM content looks correct but the icon doesn't render. (similar to the referenced http://stackoverflow.com/questions/23003278/svg-innerhtml-in-firefox-can-not-display)

// creating an element from an HTML string.
const div = document.createElement('DIV');
Copy link
Contributor

@kara kara Apr 19, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use DOM.createElement here (same with DOM.querySelector) or renderer.invokeElementMethod(')

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DOM facades are actually gone now (since we're not transpiling to dart anymore)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha

div.innerHTML = str;
const svg = <SVGElement>div.querySelector('svg');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This prompted me to file an issue on the TypeScript repo, since it could statically infer what the type is.
microsoft/TypeScript#8114

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be cool.

if (!svg) {
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')) {
Copy link
Contributor

@kara kara Apr 19, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason this isn't going through the renderer? e.g. this._renderer.getElementAttribute and this._renderer.setElementAttribute

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a service, for which there isn't a renderer instance. (each Renderer is tied to a specific component).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, forgot where I was!

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<string> {
// 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())
Copy link
Contributor

@hansl hansl Apr 19, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forget the whole thing. Apparently HTTP doesn't have a way to do that.

Note that a flatMap might be a good idea if the server returns multiple elements.

.finally(() => {
this._inProgressUrlFetches.delete(url);
})
.share();
this._inProgressUrlFetches.set(url, req);
return req;
}
}
19 changes: 19 additions & 0 deletions src/components/icon/icon.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@import "variables";
@import "default-theme";

/** The width/height of the icon element. */
$md-icon-size: 24px !default;

/**
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;
}
Loading